summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2012-11-29 22:03:29 -0800
committerMark Wells <mark@freeside.biz>2012-11-29 22:03:29 -0800
commita2a69f909cad813d7164bae805e87f5874a9fdae (patch)
tree29e426af02eb03106e55507103fc90f92fa55f61
parent226fffec6fd0154ea8798b58321d4d119341879f (diff)
broadband_snmp export: better MIB selection
-rw-r--r--FS/FS/Mason.pm1
-rw-r--r--FS/FS/part_export.pm13
-rw-r--r--FS/FS/part_export/broadband_snmp.pm150
-rwxr-xr-xhttemplate/browse/part_export.cgi48
-rw-r--r--httemplate/edit/cdr_type.cgi22
-rw-r--r--httemplate/edit/elements/part_export/broadband_snmp.html100
-rw-r--r--httemplate/edit/elements/part_export/foot.html6
-rw-r--r--httemplate/edit/elements/part_export/head.html19
-rw-r--r--httemplate/edit/part_export.cgi9
-rw-r--r--httemplate/edit/process/cdr_type.cgi1
-rw-r--r--httemplate/edit/process/part_export.cgi29
-rw-r--r--httemplate/edit/rate_time.cgi41
-rw-r--r--httemplate/elements/auto-table.html311
-rw-r--r--httemplate/elements/select-mib-popup.html170
-rw-r--r--httemplate/elements/xmlhttp.html32
-rw-r--r--httemplate/misc/xmlhttp-mib-browse.html161
16 files changed, 877 insertions, 236 deletions
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 944a483..0047016 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -56,6 +56,7 @@ if ( -e $addl_handler_use_file ) {
#use CGI::Carp qw(fatalsToBrowser);
use CGI::Cookie;
use List::Util qw( max min sum );
+ use Scalar::Util qw( blessed );
use Data::Dumper;
use Date::Format;
use Time::Local;
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
index b0f708a..ed66b41 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -615,6 +615,19 @@ sub weight {
export_info()->{$self->exporttype}->{'weight'} || 0;
}
+=item info
+
+Returns a reference to (a copy of) the export's %info hash.
+
+=cut
+
+sub info {
+ my $self = shift;
+ $self->{_info} ||= {
+ %{ export_info()->{$self->exporttype} }
+ };
+}
+
=back
=head1 SUBROUTINES
diff --git a/FS/FS/part_export/broadband_snmp.pm b/FS/FS/part_export/broadband_snmp.pm
index 44b4dba..9afca08 100644
--- a/FS/FS/part_export/broadband_snmp.pm
+++ b/FS/FS/part_export/broadband_snmp.pm
@@ -3,7 +3,7 @@ package FS::part_export::broadband_snmp;
use strict;
use vars qw(%info $DEBUG);
use base 'FS::part_export';
-use Net::SNMP qw(:asn1 :snmp);
+use SNMP;
use Tie::IxHash;
$DEBUG = 0;
@@ -11,21 +11,21 @@ $DEBUG = 0;
my $me = '['.__PACKAGE__.']';
tie my %snmp_version, 'Tie::IxHash',
- v1 => 'snmpv1',
- v2c => 'snmpv2c',
- # 3 => 'v3' not implemented
+ v1 => '1',
+ v2c => '2c',
+ # v3 unimplemented
;
-tie my %snmp_type, 'Tie::IxHash',
- i => INTEGER,
- u => UNSIGNED32,
- s => OCTET_STRING,
- n => NULL,
- o => OBJECT_IDENTIFIER,
- t => TIMETICKS,
- a => IPADDRESS,
- # others not implemented yet
-;
+#tie my %snmp_type, 'Tie::IxHash',
+# i => INTEGER,
+# u => UNSIGNED32,
+# s => OCTET_STRING,
+# n => NULL,
+# o => OBJECT_IDENTIFIER,
+# t => TIMETICKS,
+# a => IPADDRESS,
+# # others not implemented yet
+#;
tie my %options, 'Tie::IxHash',
'version' => { label=>'SNMP version',
@@ -33,14 +33,11 @@ tie my %options, 'Tie::IxHash',
options => [ keys %snmp_version ],
},
'community' => { label=>'Community', default=>'public' },
- (
- map { $_.'_command',
- { label => ucfirst($_) . ' commands',
- type => 'textarea',
- default => '',
- }
- } qw( insert delete replace suspend unsuspend )
- ),
+
+ 'action' => { multiple=>1 },
+ 'oid' => { multiple=>1 },
+ 'value' => { multiple=>1 },
+
'ip_addr_change_to_new' => {
label=>'Send IP address changes to new address',
type=>'checkbox'
@@ -51,28 +48,14 @@ tie my %options, 'Tie::IxHash',
%info = (
'svc' => 'svc_broadband',
'desc' => 'Send SNMP requests to the service IP address',
+ 'config_element' => '/edit/elements/part_export/broadband_snmp.html',
'options' => \%options,
'no_machine' => 1,
'weight' => 10,
'notes' => <<'END'
Send one or more SNMP SET requests to the IP address registered to the service.
-Enter one command per line. Each command is a target OID, data type flag,
-and value, separated by spaces.
-The data type flag is one of the following:
-<font size="-1"><ul>
-<li><i>i</i> = INTEGER</li>
-<li><i>u</i> = UNSIGNED32</li>
-<li><i>s</i> = OCTET-STRING (as ASCII)</li>
-<li><i>a</i> = IPADDRESS</li>
-<li><i>n</i> = NULL</li></ul>
The value may interpolate fields from svc_broadband by prefixing the field
name with <b>$</b>, or <b>$new_</b> and <b>$old_</b> for replace operations.
-The value may contain whitespace; quotes are not necessary.<br>
-<br>
-For example, to set the SNMPv2-MIB "sysName.0" object to the string
-"svc_broadband" followed by the service number, use the following
-command:<br>
-<pre>1.3.6.1.2.1.1.5.0 s svc_broadband$svcnum</pre><br>
END
);
@@ -105,19 +88,18 @@ sub export_command {
my $self = shift;
my ($action, $svc_new, $svc_old) = @_;
- my $command_text = $self->option($action.'_command');
- return if !length($command_text);
-
- warn "$me parsing ${action}_command:\n" if $DEBUG;
+ my @a = split("\n", $self->option('action'));
+ my @o = split("\n", $self->option('oid'));
+ my @v = split("\n", $self->option('value'));
my @commands;
- foreach (split /\n/, $command_text) {
- my ($oid, $type, $value) = split /\s/, $_, 3;
- $oid =~ /^(\d+\.)*\d+$/ or die "invalid OID '$oid'\n";
- my $typenum = $snmp_type{$type} or die "unknown data type '$type'\n";
- $value = '' if !defined($value); # allow sending an empty string
+ warn "$me parsing $action commands:\n" if $DEBUG;
+ while (@a) {
+ my $oid = shift @o;
+ my $value = shift @v;
+ next unless shift(@a) eq $action; # ignore commands for other actions
$value = $self->substitute($value, $svc_new, $svc_old);
- warn "$me $oid $type $value\n" if $DEBUG;
- push @commands, $oid, $typenum, $value;
+ warn "$me $oid :=$value\n" if $DEBUG;
+ push @commands, $oid, $value;
}
my $ip_addr = $svc_new->ip_addr;
@@ -128,13 +110,13 @@ sub export_command {
warn "$me opening session to $ip_addr\n" if $DEBUG;
my %opt = (
- -hostname => $ip_addr,
- -community => $self->option('community'),
- -timeout => $self->option('timeout') || 20,
+ DestHost => $ip_addr,
+ Community => $self->option('community'),
+ Timeout => ($self->option('timeout') || 20) * 1000,
);
my $version = $self->option('version');
- $opt{-version} = $snmp_version{$version} or die 'invalid version';
- $opt{-varbindlist} = \@commands; # just for now
+ $opt{Version} = $snmp_version{$version} or die 'invalid version';
+ $opt{VarList} = \@commands; # for now
$self->snmp_queue( $svc_new->svcnum, %opt );
}
@@ -151,16 +133,22 @@ sub snmp_queue {
sub snmp_request {
my %opt = @_;
- my $varbindlist = delete $opt{-varbindlist};
- my ($session, $error) = Net::SNMP->session(%opt);
- die "Couldn't create SNMP session: $error" if !$session;
+ my $flatvarlist = delete $opt{VarList};
+ my $session = SNMP::Session->new(%opt);
warn "$me sending SET request\n" if $DEBUG;
- my $result = $session->set_request( -varbindlist => $varbindlist );
- $error = $session->error();
- $session->close();
- if (!defined $result) {
+ my @varlist;
+ while (@$flatvarlist) {
+ my @this = splice(@$flatvarlist, 0, 2);
+ push @varlist, [ $this[0], 0, $this[1], undef ];
+ # XXX new option to choose the IID (array index) of the object?
+ }
+
+ $session->set(\@varlist);
+ my $error = $session->{ErrorStr};
+
+ if ( $session->{ErrorNum} ) {
die "SNMP request failed: $error\n";
}
}
@@ -181,4 +169,46 @@ sub substitute {
$value;
}
+sub _upgrade_exporttype {
+ eval 'use FS::Record qw(qsearch qsearchs)';
+ # change from old style with numeric oid, data type flag, and value
+ # on consecutive lines
+ foreach my $export (qsearch('part_export',
+ { exporttype => 'broadband_snmp' } ))
+ {
+ # for the new options
+ my %new_options = (
+ 'action' => [],
+ 'oid' => [],
+ 'value' => [],
+ );
+ foreach my $action (qw(insert replace delete suspend unsuspend)) {
+ my $old_option = qsearchs('part_export_option',
+ { exportnum => $export->exportnum,
+ optionname => $action.'_command' } );
+ next if !$old_option;
+ my $text = $old_option->optionvalue;
+ my @commands = split("\n", $text);
+ foreach (@commands) {
+ my ($oid, $type, $value) = split /\s/, $_, 3;
+ push @{$new_options{action}}, $action;
+ push @{$new_options{oid}}, $oid;
+ push @{$new_options{value}}, $value;
+ }
+ my $error = $old_option->delete;
+ warn "error migrating ${action}_command option: $error\n" if $error;
+ }
+ foreach (keys(%new_options)) {
+ my $new_option = FS::part_export_option->new({
+ exportnum => $export->exportnum,
+ optionname => $_,
+ optionvalue => join("\n", @{ $new_options{$_} })
+ });
+ my $error = $new_option->insert;
+ warn "error inserting '$_' option: $error\n" if $error;
+ }
+ } #foreach $export
+ '';
+}
+
1;
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
index b7ecc00..91238a0 100755
--- a/httemplate/browse/part_export.cgi
+++ b/httemplate/browse/part_export.cgi
@@ -43,14 +43,56 @@ function part_export_areyousure(href) {
<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
<% itable() %>
% my %opt = $part_export->options;
-% foreach my $opt ( keys %opt ) {
+% my $defs = $part_export->info->{options};
+% my %multiples;
+% foreach my $opt (keys %$defs) { # is a Tie::IxHash
+% my $group = $defs->{$opt}->{multiple};
+% if ( $group ) {
+% my @values = split("\n", $opt{$opt});
+% $multiples{$group} ||= [];
+% push @{ $multiples{$group} }, [ $opt, @values ] if @values;
+% delete $opt{$opt};
+% } elsif (length($opt{$opt})) { # the normal case
+%# foreach my $opt ( keys %opt ) {
<TR>
<TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>:&nbsp;</TD>
<TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD>
</TR>
-% }
-
+% delete $opt{$opt};
+% }
+% }
+% # now any that are somehow not in the options list
+% foreach my $opt (keys %opt) {
+% if ( length($opt{$opt}) ) {
+ <TR>
+ <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>:&nbsp;</TD>
+ <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD>
+ </TR>
+% }
+% }
+% # now show any multiple-option groups
+% foreach (sort keys %multiples) {
+% my $set = $multiples{$_};
+ <TR><TD ALIGN="center" COLSPAN=2><TABLE CLASS="grid">
+ <TR>
+% foreach my $col (@$set) {
+ <TH><% shift @$col %></TH>
+% }
+ </TR>
+% while ( 1 ) {
+ <TR>
+% my $end = 1;
+% foreach my $col (@$set) {
+ <TD><% shift @$col %></TD>
+% $end = 0 if @$col;
+% }
+ </TR>
+% last if $end;
+% }
+ </TABLE></TD></TR>
+% } #foreach keys %multiples
+
</TABLE>
</TD>
diff --git a/httemplate/edit/cdr_type.cgi b/httemplate/edit/cdr_type.cgi
index 5d2c662..c696106 100644
--- a/httemplate/edit/cdr_type.cgi
+++ b/httemplate/edit/cdr_type.cgi
@@ -7,11 +7,24 @@ calls and SMS messages. Each CDR type must have a set of rates
configured in the rate tables.
<BR>
<FORM METHOD="POST" ACTION="<% "${p}edit/process/cdr_type.cgi" %>">
-<% include('/elements/auto-table.html',
- 'header' => [ 'Type#', 'Name' ],
- 'fields' => [ qw( cdrtypenum cdrtypename ) ],
+<TABLE ID="AutoTable" BORDER=0 CELLSPACING=0>
+ <TR>
+ <TH>Type#</TH>
+ <TH>Name</TH>
+ </TR>
+ <TR ID="cdr_template">
+ <TD>
+ <INPUT NAME="cdrtypenum" SIZE=16 MAXLENGTH=16 ALIGN="right">
+ </TD>
+ <TD>
+ <INPUT NAME="cdrtypename" SIZE=16 MAXLENGTH=16>
+ </TD>
+ </TR>
+<& /elements/auto-table.html,
+ 'template_row' => 'cdr_template',
'data' => \@data,
- ) %>
+&>
+</TABLE>
<INPUT TYPE="submit" VALUE="Apply changes"> </FORM> <BR>
<% include('/elements/footer.html') %>
<%init>
@@ -20,7 +33,6 @@ die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
my @data = (
- map { [ $_->cdrtypenum, $_->cdrtypename ] }
qsearch({
'table' => 'cdr_type',
'hashref' => {},
diff --git a/httemplate/edit/elements/part_export/broadband_snmp.html b/httemplate/edit/elements/part_export/broadband_snmp.html
new file mode 100644
index 0000000..8df0b8e
--- /dev/null
+++ b/httemplate/edit/elements/part_export/broadband_snmp.html
@@ -0,0 +1,100 @@
+<%doc>
+</%doc>
+<& head.html, %opt &>
+<INPUT TYPE="hidden" NAME="options" VALUE="community,version,ip_addr_change_to_new,timeout">
+<& /elements/tr-select.html,
+ label => 'SNMP version',
+ field => 'version',
+ options => [ '', 'v1', 'v2c' ],
+ labels => { v1 => '1', v2c => '2c' },
+ curr_value => $part_export->option('version') &>
+<& /elements/tr-input-text.html,
+ label => 'Community',
+ field => 'community',
+ curr_value => $part_export->option('community'),
+&>
+<& /elements/tr-checkbox.html,
+ label => 'Send IP address changes to new address',
+ field => 'ip_addr_change_to_new',
+ value => 1,
+ curr_value => $part_export->option('ip_addr_change_to_new'),
+&>
+<& /elements/tr-input-text.html,
+ label => 'Timeout (seconds)',
+ field => 'timeout',
+ curr_value => $part_export->option('timeout'),
+&>
+</TABLE>
+<script type="text/javascript">
+function open_select_mib(obj) {
+ nd(1); // if there's already one open, close it
+ var rownum = obj.rownum;
+ var curr_oid = obj.value || '';
+ var url = '<%$fsurl%>/elements/select-mib-popup.html?' +
+ 'callback=receive_mib;' +
+ 'arg=' + rownum +
+ ';curr_value=' + curr_oid;
+ overlib(
+ OLiframeContent(url, 550, 450, '<% $popup_name %>', 0, 'auto'),
+ CAPTION, 'Select MIB object', STICKY, AUTOSTATUSCAP,
+ MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK,
+ BGCOLOR, '#333399', CGCOLOR, '#333399',
+ CLOSETEXT, 'Close'
+ );
+}
+function receive_mib(obj, rownum) {
+ //console.log(JSON.stringify(obj));
+ // we don't really need the numeric OID or any of the other properties
+ document.getElementById('oid'+rownum).value = obj.fullname;
+ document.getElementById('datatype'+rownum).innerHTML = obj.type;
+}
+</script>
+
+<table bgcolor="#cccccc" border=0 cellspacing=3>
+<TR>
+ <TH>Action</TH>
+ <TH>Object</TH>
+ <TH>Type</TH>
+ <TH>Value</TH>
+</TR>
+<TR id="mytemplate">
+ <TD>
+ <SELECT NAME="action">
+% foreach ('', qw(insert delete replace suspend unsuspend)) {
+ <OPTION VALUE="<%$_%>"><%$_%></OPTION>
+% }
+ </SELECT>
+ </TD>
+ <TD>
+ <INPUT NAME="oid" ID="oid" SIZE="60" onclick="open_select_mib(this)">
+ </TD>
+ <TD>
+ <SPAN ID="datatype"></SPAN>
+ </TD>
+ <TD>
+ <INPUT NAME="value" ID="value">
+ </TD>
+</TR>
+<& /elements/auto-table.html,
+ template_row => 'mytemplate',
+ fieldorder => ['action', 'oid', 'value'],
+ data => \@data,
+&>
+<INPUT TYPE="hidden" NAME="multi_options" VALUE="action,oid,value">
+<& foot.html, %opt &>
+<%init>
+my %opt = @_;
+my $part_export = $opt{part_export} || FS::part_export->new;
+
+my @actions = split("\n", $part_export->option('action'));
+my @oids = split("\n", $part_export->option('oid'));
+my @values = split("\n", $part_export->option('value'));
+
+my @data;
+while (@actions or @oids or @values) {
+ my @thisrow = (shift(@actions), shift(@oids), shift(@values));
+ push @data, \@thisrow if grep length($_), @thisrow;
+}
+
+my $popup_name = 'popup-'.time."-$$-".rand() * 2**32;
+</%init>
diff --git a/httemplate/edit/elements/part_export/foot.html b/httemplate/edit/elements/part_export/foot.html
new file mode 100644
index 0000000..9cb8073
--- /dev/null
+++ b/httemplate/edit/elements/part_export/foot.html
@@ -0,0 +1,6 @@
+</TABLE>
+<INPUT TYPE="hidden" NAME="nodomain" VALUE="<% $opt{export_info}{nodomain} %>">
+<INPUT TYPE="submit" VALUE="<% $opt{part_export}->exportnum ? 'Apply changes' : 'Add export' %>">
+<%init>
+my %opt = @_;
+</%init>
diff --git a/httemplate/edit/elements/part_export/head.html b/httemplate/edit/elements/part_export/head.html
new file mode 100644
index 0000000..cb0ab89
--- /dev/null
+++ b/httemplate/edit/elements/part_export/head.html
@@ -0,0 +1,19 @@
+% if ( $export_info->{no_machine} ) {
+<INPUT TYPE="hidden" NAME="machine" VALUE="">
+<INPUT TYPE="hidden" NAME="svc_machine" VALUE="N">
+% } else {
+% # clone this from edit/part_export.cgi if this case ever gets used
+% }
+<INPUT TYPE="hidden" NAME="exporttype" VALUE="<%$layer |h%>">
+<% ntable('cccccc', 2) %>
+<TR>
+ <TD ALIGN="right" ><% emt('Description') %></TD>
+ <TD BGCOLOR="#ffffff" WIDTH="600"><% $notes %></TD>
+</TR>
+<%init>
+my %opt = @_;
+my $layer = $opt{layer};
+my $part_export = $opt{part_export};
+my $export_info = $opt{export_info};
+my $notes = $opt{notes} || $export_info->{notes};
+</%init>
diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi
index 0407ee7..4dd253b 100644
--- a/httemplate/edit/part_export.cgi
+++ b/httemplate/edit/part_export.cgi
@@ -62,6 +62,15 @@ my $widget = new HTML::Widgets::SelectLayers(
'html_between' => "</TD></TR></TABLE>\n",
'layer_callback' => sub {
my $layer = shift;
+ # create 'config_element' to generate the whole layer with a Mason component
+ if ( my $include = $exports->{$layer}{config_element} ) {
+ # might need to adjust the scope of this at some point
+ return $m->scomp($include,
+ part_export => $part_export,
+ layer => $layer,
+ export_info => $exports->{$layer}
+ );
+ }
my $html = qq!<INPUT TYPE="hidden" NAME="exporttype" VALUE="$layer">!.
ntable("#cccccc",2);
diff --git a/httemplate/edit/process/cdr_type.cgi b/httemplate/edit/process/cdr_type.cgi
index b661de7..ba9881d 100644
--- a/httemplate/edit/process/cdr_type.cgi
+++ b/httemplate/edit/process/cdr_type.cgi
@@ -10,7 +10,6 @@ die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
my %vars = $cgi->Vars;
-warn Dumper(\%vars)."\n";
my %old = map { $_->cdrtypenum => $_ } qsearch('cdr_type', {});
diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi
index 6432d6b..bcb9c0d 100644
--- a/httemplate/edit/process/part_export.cgi
+++ b/httemplate/edit/process/part_export.cgi
@@ -13,15 +13,40 @@ my $exportnum = $cgi->param('exportnum');
my $old = qsearchs('part_export', { 'exportnum'=>$exportnum } ) if $exportnum;
+my %vars = $cgi->Vars;
#fixup options
#warn join('-', split(',',$cgi->param('options')));
my %options = map {
- my @values = $cgi->param($_);
- my $value = scalar(@values) > 1 ? join (' ', @values) : $values[0];
+ my $value = $vars{$_};
+ $value =~ s/\0/ /g; # deal with multivalued options
$value =~ s/\r\n/\n/g; #browsers? (textarea)
$_ => $value;
} split(',', $cgi->param('options'));
+# deal with multiline options
+# %vars should never contain incomplete rows, but just in case it does,
+# we make a list of all the row indices that contain values, and
+# then write a line in each option for each row, even if it's empty.
+# This ensures that all values with the same row index line up.
+my %optionrows;
+foreach my $option (split(',', $cgi->param('multi_options'))) {
+ $optionrows{$option} = {};
+ my %values; # bear with me
+ for (keys %vars) {
+ /^$option(\d+)/ or next;
+ $optionrows{$option}{$1} = $vars{$option.$1};
+ $optionrows{_ALL_}{$1} = 1 if length($vars{$option.$1});
+ }
+}
+foreach my $option (split(',', $cgi->param('multi_options'))) {
+ my $value = '';
+ foreach my $row (sort keys %{$optionrows{_ALL_}}) {
+ $value .= ($optionrows{$option}{$row} || '') . "\n";
+ }
+ chomp($value);
+ $options{$option} = $value;
+}
+
my $new = new FS::part_export ( {
map {
$_, scalar($cgi->param($_));
diff --git a/httemplate/edit/rate_time.cgi b/httemplate/edit/rate_time.cgi
index 7ee39ef..9e6b873 100644
--- a/httemplate/edit/rate_time.cgi
+++ b/httemplate/edit/rate_time.cgi
@@ -15,12 +15,34 @@
<TD><INPUT TYPE="text" NAME="ratetimename" VALUE="<% $rate_time ? $rate_time->ratetimename : '' %>"></TD>
</TR>
</TABLE>
-<% include('/elements/auto-table.html',
- 'header' => [ '', 'Start','','', '','End','','' ],
- 'fields' => [ qw(sd sh sm sa ed eh em ea) ],
- 'select' => [ ($day, $hour, $min, $ampm) x 2 ],
- 'data' => \@data,
- ) %>
+<TABLE>
+ <TR>
+ <TH COLSPAN=4 ALIGN="center">Start</TH>
+ <TH COLSPAN=4 ALIGN="center">End</TH>
+ </TR>
+ <TR id="mytemplate">
+% for my $pre (qw(s e)) {
+% for my $f (qw(d h m a)) { # day, hour, minute, am/pm
+ <TD>
+ <SELECT NAME="<%$pre.$f%>">
+% my $i = 0;
+% while ($i < @{ $choices{$f} }) {
+ <OPTION VALUE="<%$choices{$f}[$i]%>">
+% $i++;
+ <%$choices{$f}[$i]%></OPTION>
+% $i++;
+% }
+ </SELECT>
+ </TD>
+% } #$f
+% } #$pre
+ </TR>
+<& /elements/auto-table.html,
+ 'template_row' => 'mytemplate',
+ 'data' => \@data,
+ 'fieldorder' => [qw(sd sh sm sa ed eh em ea)],
+&>
+</TABLE>
<INPUT TYPE="submit" VALUE="<% $rate_time ? 'Apply changes' : 'Add period'%>">
</FORM>
<BR>
@@ -42,7 +64,12 @@ my $day = [ 0 => 'Sun',
my $hour = [ map( {$_, sprintf('%02d',$_) } 12, 1..11 )];
my $min = [ map( {$_, sprintf('%02d',$_) } 0,30 )];
my $ampm = [ 0 => 'AM', 1 => 'PM' ];
-
+my %choices = (
+ 'd' => $day,
+ 'h' => $hour,
+ 'm' => $min,
+ 'a' => $ampm,
+);
if($ratetimenum) {
$action = 'Edit';
$rate_time = qsearchs('rate_time', {ratetimenum => $ratetimenum})
diff --git a/httemplate/elements/auto-table.html b/httemplate/elements/auto-table.html
index 4922274..ed01109 100644
--- a/httemplate/elements/auto-table.html
+++ b/httemplate/elements/auto-table.html
@@ -1,166 +1,181 @@
<%doc>
-
-Example:
-<% include('/elements/auto-table.html',
-
- ###
- # required
- ###
-
- 'header' => [ '#', 'Item', 'Amount' ],
- 'fields' => [ 'id', 'name', 'amount' ],
-
- ###
- # highly recommended
- ###
-
- 'size' => [ 4, 12, 8 ],
- 'maxl' => [ 4, 12, 8 ],
- 'align' => [ 'right', 'left', 'right' ],
-
- ###
- # optional
- ###
-
- 'data' => [ [ 1, 'Widget', 25 ],
- [ 12, 'Super Widget, 7 ] ],
- #or
- 'records' => [ qsearch('item', { } ) ],
- # or any other array of FS::Record objects
-
- 'select' => [ '',
- [ 1 => 'option 1',
- 2 => 'option 2', ...
- ], # options for second field
- '' ],
-
- 'prefix' => 'mytable_',
-) %>
-
-Values will be passed through as "mytable_id1", etc.
+(within a form)
+<table>
+<tr>
+ <th>Field 1</th>
+ <th>Field 2</th>
+</tr>
+<tr id="mytemplate">
+ <td><input type="text" name="field1"></td>
+ <td><select name="field2">...</td>
+ ...
+</tr>
+</table>
+<& /elements/auto-table.html,
+ table => 'mytable',
+ template_row = 'mytemplate',
+ rows => [
+ { field1 => 'foo', field2 => 'CA', ... },
+ { field1 => 'bar', field2 => 'TX', ... }, ...
+ ],
+&>
+
+ or if you prefer:
+...
+ fieldorder => [ 'field1', 'field2', ... ],
+ rows => [
+ [ 'foo', 'CA' ],
+ [ 'bar', 'TX' ],
+ ],
+
+In the process/ handler, something like:
+my @rows;
+my %vars = $cgi->Vars;
+for my $k ( keys %vars ) {
+ $k =~ /^${pre}magic(\d+)$/ or next;
+ my $rownum = $1;
+ # find all submitted names ending in this rownum
+ my %thisrow =
+ map { $_ => $vars{$_} }
+ grep /^(.*[\d])$rownum$/, keys %vars;
+ $thisrow->{num} = delete $thisrow{"${pre}magic$rownum"};
+ push @rows, $thisrow;
+}
</%doc>
-
-<TABLE ID="<% $prefix %>AutoTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
- <TR>
-% foreach (@header) {
- <TH><% $_ %></TH>
-% }
- </TR>
-% my $row = 0;
-% for ( $row = 0; $row < scalar @data; $row++ ) {
- <TR>
-% my $col = 0;
-% for ( $col = 0; $col < scalar @fields; $col++ ) {
-% my $id = $prefix . $fields[$col];
-% # don't suffix rownum in the final, blank row
-% $id .= $row if $row < (scalar @data) - 1;
- <TD>
-% my @o = @{ $select[$col] };
-% if( @o ) {
- <SELECT NAME="<% $id %>" ID="<% $id %>">
-% while(@o) {
-% my $val = shift @o;
- <OPTION VALUE=<% $val %><%
-$val eq $data[$row][$col] ? ' SELECTED' : ''%>><% shift @o %></OPTION>
-% }
- </SELECT>
-% }
-% else {
- <INPUT TYPE = "text"
- NAME = "<% $id %>"
- ID = "<% $id %>"
- SIZE = <% $size[$col] %>
- MAXLENGTH = <% $maxl[$col] %>
- STYLE = "text-align:<% $align[$col] %>"
- VALUE = "<% $data[$row][$col] %>"
-% if( $opt{'autoadd'} ) {
- onchange = "possiblyAddRow(this);"
-% }
- >
- </TD>
-% }
-% }
- <TD>
- <IMG SRC = "<% "${p}images/cross.png" %>"
- ALT = "X"
- onclick = "deleteRow(this);"
- >
- </TD>
- </TR>
-% }
-</TABLE>
-% if( !$opt{'autoadd'} ) {
-<INPUT TYPE="button" VALUE="Add" onclick="<% $prefix %>addRow();"><BR>
-% }
-
-<SCRIPT TYPE="text/javascript">
- var <% $prefix %>rownum = <% $row %>;
- var <% $prefix %>table = document.getElementById('<% $prefix %>AutoTable');
- // last row is initially blank, clone it and remove it
- var <% $prefix %>_blank =
- <% $prefix %>table.rows[<% $prefix %>table.rows.length-1].cloneNode(true);
-% if( !$opt{'autoadd'} ) {
- <% $prefix %>table.deleteRow(<% $prefix %>table.rows.length-1);
-% }
-
-
-
- function rownum_of(obj) {
- return (obj.parentNode.parentNode.sectionRowIndex);
+<tbody id="<%$pre%>autotable"></tbody>
+<script type="text/javascript">
+var <%$pre%>template;
+var <%$pre%>tbody;
+var <%$pre%>next_rownum;
+var <%$pre%>set_rownum;
+var <%$pre%>addRow;
+var <%$pre%>deleteRow;
+var <%$pre%>fieldorder = <% to_json($fieldorder) %>;
+
+function <%$pre%>possiblyAddRow_factory(obj) {
+ var callback = obj.onchange;
+ return function() {
+ if ( obj.rownum == <%$pre%>tbody.lastChild.rownum ) {
+ // then this is the last row, and it's being changed, so spawn a new row
+ <%$pre%>addRow();
+ }
+ if ( callback ) {
+ callback.apply(obj);
+ }
}
+}
- function <% $prefix %>possiblyAddRow(obj) {
- if ( <% $prefix %>rownum == rownum_of(obj) ) {
- <% $prefix %>addRow();
+function <%$pre%>set_rownum(obj, rownum) {
+ obj.rownum = rownum;
+ if ( obj.id ) {
+ obj.id = obj.id + rownum;
+ }
+ if ( obj.name ) {
+ obj.name = obj.name + rownum;
+ // also, in this case it's a form field that will be part of the record
+ // so set up an onchange handler
+ obj.onchange = <%$pre%>possiblyAddRow_factory(obj);
+ }
+ for (var i = 0; i < obj.children.length; i++) {
+ if ( obj.children[i] instanceof Node ) {
+ <%$pre%>set_rownum(obj.children[i], rownum);
}
}
+}
- function <% $prefix %>addRow() {
- var row = <% $prefix %>table.insertRow(-1);
- var cells = <% $prefix %>_blank.cells;
- for (i=0; i<cells.length; i++) {
- var node = row.appendChild(cells[i].cloneNode(true));
- var input = node.children[0];
- input.id = input.id + row.sectionRowIndex;
- input.name = input.name + row.sectionRowIndex;
+function <%$pre%>addRow(data) {
+ // duplicate the node
+ // warning: cloneNode doesn't clone event handlers that were set through
+ // the DOM
+ // if 'data' is an object, prepopulate the row's fields with the object's
+ // elements
+ // returns the rownum of the new row
+ var row = <%$pre%>template.cloneNode(true);
+ <%$pre%>tbody.appendChild(row);
+ var this_rownum = <%$pre%>next_rownum;
+ <%$pre%>set_rownum(row, this_rownum);
+ if(data instanceof Array) {
+ for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) {
+ var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0];
+ if (el) {
+ el.value = data[i];
+ }
+ }
+ } else if (data instanceof Object) {
+ for (var field in data) {
+ var el = document.getElementsByName(field + this_rownum)[0];
+ if (el) {
+ el.value = data[field];
+% # doesn't work for checkbox
+ }
}
- <% $prefix %>rownum++;
+ } // else nothing
+ <%$pre%>next_rownum++;
+ return this_rownum;
+}
+
+function <%$pre%>deleteRow(rownum) {
+ if ( rownum == <%$pre%>tbody.lastChild.rownum ) {
+ // if this is the last row, spawn another one after it
+ <%$pre%>addRow();
}
+ var r = document.getElementById('<%$pre%>row' + rownum);
+ <%$pre%>tbody.removeChild(r);
+}
- function deleteRow(obj) {
- if(<% $prefix %>rownum == rownum_of(obj)) {
- <% $prefix %>addRow();
- }
- <% $prefix %>table.deleteRow(rownum_of(obj));
- <% $prefix %>rownum--;
- return(false);
+function <%$pre%>init() {
+ <%$pre%>template = document.getElementById(<% $template_row |js_string%>);
+ <%$pre%>tbody = document.getElementById('<%$pre%>autotable');
+ <%$pre%>next_rownum = <%$pre%>template.sectionRowIndex;
+ // detach the template row
+ var table = <%$pre%>template.parentNode;
+ table.removeChild(<%$pre%>template);
+ // give it an id
+ <%$pre%>template.id = <%$pre |js_string%> + 'row';
+ // and a magic identifier so we know it's been submitted
+ var magic = document.createElement('INPUT');
+ magic.setAttribute('type', 'hidden');
+ magic.setAttribute('name', '<%$pre%>magic');
+ magic.value = '1';
+ // and a delete button
+%# should this be enclosed in an actual <button> for aesthetics?
+ var delete_button = document.createElement('IMG');
+ delete_button.id = 'delete_button';
+ delete_button.src = '<%$fsurl%>images/cross.png';
+ delete_button.alt = 'X';
+ // use an inline string for this so that it will be cloned properly
+ delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);");
+ var delete_cell = document.createElement('TD');
+ delete_cell.appendChild(delete_button);
+ delete_cell.appendChild(magic); // it has to go somewhere
+ <%$pre%>template.appendChild(delete_cell);
+
+ // preload rows
+ var rows = <% to_json(\@rows) %>;
+ for (var i = 0; i < rows.length; i++) {
+ <%$pre%>addRow(rows[i]);
}
-</SCRIPT>
+ <%$pre%>addRow();
+}
+<%$pre%>init();
+</script>
<%init>
my %opt = @_;
-
-my @header = @{ $opt{'header'} };
-my @fields = @{ $opt{'fields'} };
-my @data = ();
-if($opt{'data'}) {
- @data = @{ $opt{'data'} };
-}
-elsif($opt{'records'}) {
- foreach my $rec (@{ $opt{'records'} }) {
- push @data, [ map { $rec->getfield($_) } @fields ];
+my $pre = '';
+$pre = $opt{'table'} . '_' if $opt{'table'};
+my $template_row = $opt{'template_row'}
+ or die "auto-table requires template_row\n"; # a DOM id
+
+my %vars = $cgi->Vars;
+# rows that we will preload, as hashrefs of name => value
+my @rows = @{ $opt{'data'} || [] };
+foreach (@rows) {
+ # allow an array of FS::Record objects to be passed
+ if ( blessed($_) and $_->isa('FS::Record') ) {
+ $_ = $_->hashref;
}
}
-# else @data = ();
-push @data, [ map {''} @fields ]; # make a blank row
-
-my $prefix = $opt{'prefix'};
-my @size = $opt{'size'} ? @{ $opt{'size'} } : (map {16} @fields);
-my @maxl = $opt{'maxl'} ? @{ $opt{'maxl'} } : @size;
-my @align = $opt{'align'} ? @{ $opt{'align'} } : (map {'right'} @fields);
-my @select = @{ $opt{'select'} || [] };
-foreach (0..scalar(@fields)-1) {
- $select[$_] ||= [];
-}
+my $fieldorder = $opt{'fieldorder'} || [];
</%init>
diff --git a/httemplate/elements/select-mib-popup.html b/httemplate/elements/select-mib-popup.html
new file mode 100644
index 0000000..f8e3ae3
--- /dev/null
+++ b/httemplate/elements/select-mib-popup.html
@@ -0,0 +1,170 @@
+<& /elements/header-popup.html &>
+<TABLE WIDTH="100%">
+<TR>
+ <TD ALIGN="right">Module:</TD>
+ <TD><SELECT ID="select_module"></SELECT></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Object:</TD>
+ <TD><INPUT TYPE="text" NAME="path" ID="input_path"></TD>
+</TR>
+<TR>
+ <TD COLSPAN=2>
+ <SELECT STYLE="width:100%" SIZE=12 ID="select_path"></SELECT>
+ </TD>
+</TR>
+<TR>
+ <TH COLSPAN=2 ID="mib_objectID"></TH>
+</TR>
+<TR>
+ <TD ALIGN="right">Module: </TD><TD ID="mib_moduleID"></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Data type: </TD><TD ID="mib_type"></TD>
+</TR>
+<TR>
+ <TH COLSPAN=2>
+ <BUTTON ID="submit_button" onclick="submit()" DISABLED=1>Continue</BUTTON>
+ </TH>
+</TR>
+</TABLE>
+<& /elements/xmlhttp.html,
+ url => $p.'misc/xmlhttp-mib-browse.html',
+ subs => [qw( search get_module_list )],
+&>
+<SCRIPT TYPE="text/javascript">
+
+var selected_mib;
+
+function show_info(state) {
+ document.getElementById('mib_objectID').style.display =
+ document.getElementById('mib_moduleID').style.display =
+ document.getElementById('mib_type').style.display =
+ state ? 'block' : 'none';
+}
+
+function clear_list() {
+ var select_path = document.getElementById('select_path');
+ select_path.options.length = 0;
+}
+
+function add_item(value) {
+ var select_path = document.getElementById('select_path');
+ var input_path = document.getElementById('input_path');
+ var opt = document.createElement('option');
+ var v = value;
+ if ( v.match(/-$/) ) {
+ opt.className = 'leaf';
+ v = v.substring(0, v.length - 1);
+ }
+ opt.value = opt.text = v;
+ opt.selected = (input_path.value == v);
+ select_path.add(opt, null);
+}
+
+var timerID = 0;
+
+function populate(json_result) {
+ var result = JSON.parse(json_result);
+ clear_list();
+ for (var x in result['choices']) {
+ opt = document.createElement('option');
+ add_item(result['choices'][x]);
+ }
+ if ( result['objectID'] ) {
+ selected_mib = result;
+ show_info(true);
+ // show details on the selected node
+ document.getElementById('mib_objectID').innerHTML = result.objectID;
+ document.getElementById('mib_moduleID').innerHTML = result.moduleID;
+ document.getElementById('mib_type').innerHTML = result.type;
+ document.getElementById('submit_button').disabled = !result.type;
+ } else {
+ selected_mib = undefined;
+ show_info(false);
+ }
+}
+
+function populate_modules(json_result) {
+ var result = JSON.parse(json_result);
+ var select_module = document.getElementById('select_module');
+ var opt = document.createElement('option');
+ opt.value = 'ANY';
+ opt.text = '(any)';
+ select_module.add(opt, null);
+ for (var x in result['modules']) {
+ opt = document.createElement('option');
+ opt.value = opt.text = result['modules'][x];
+ select_module.add(opt, null);
+ }
+}
+
+function dispatch_search() {
+ // called from the interval timer
+ var search_string = document.getElementById('select_module').value + ':' +
+ document.getElementById('input_path').value;
+
+ search(search_string, populate);
+}
+
+function delayed_search() {
+ // onkeyup handler for the text input
+ // 500ms after the user stops typing, send the search request
+ if (timerID != 0) {
+ clearTimeout(timerID);
+ }
+ timerID = setTimeout(dispatch_search, 500);
+}
+
+function handle_choose_object() {
+ // onchange handler for the selector
+ // when the user picks an option, set the text input to that, and then
+ // search for it as though it was entered
+ var input_path = document.getElementById('input_path');
+ input_path.value = this.value;
+ dispatch_search();
+}
+
+function handle_choose_module() {
+ input_path.value = ''; // just to avoid confusion
+ delayed_search();
+}
+
+function submit() {
+% if ( $callback ) {
+ <% $callback %>;
+ parent.nd(1); // close popup
+% } else {
+ alert(document.getElementById('input_path').value);
+% }
+}
+
+var input_path = document.getElementById('input_path');
+input_path.onkeyup = delayed_search;
+var select_path = document.getElementById('select_path');
+select_path.onchange = handle_choose_object;
+var select_module = document.getElementById('select_module');
+select_module.onchange = handle_choose_module;
+% if ( $cgi->param('curr_value') ) {
+input_path.value = <% $cgi->param('curr_value') |js_string %>;
+% }
+dispatch_search();
+get_module_list('', populate_modules);
+
+</SCRIPT>
+<& /elements/footer.html &>
+<%init>
+my $callback = 'alert("(no callback defined)" + selected_mib.stringify)';
+$cgi->param('callback') =~ /^(\w+)$/;
+if ( $1 ) {
+ # construct the JS function call expresssion
+ $callback = 'window.parent.' . $1 . '(selected_mib';
+ foreach ($cgi->param('arg')) {
+ # pass-through arguments
+ /^(\w+)$/ or next;
+ $callback .= ",'$1'";
+ }
+ $callback .= ')';
+}
+
+</%init>
diff --git a/httemplate/elements/xmlhttp.html b/httemplate/elements/xmlhttp.html
index ac6f991..a9e65c7 100644
--- a/httemplate/elements/xmlhttp.html
+++ b/httemplate/elements/xmlhttp.html
@@ -14,14 +14,15 @@ Example:
);
</%doc>
-<% include( '/elements/rs_init_object.html' ) %>
+<& /elements/rs_init_object.html &>
+<& /elements/init_overlib.html &>
<SCRIPT TYPE="text/javascript">
% foreach my $func ( @{$opt{'subs'}} ) {
%
% my $furl = $url;
% $furl =~ s/\"/\\\\\"/; #javascript escape
-%
+%#"
%
@@ -66,15 +67,26 @@ Example:
} else {
var data = xmlhttp.responseText;
//alert('received response: ' + data);
- a[a.length-1](data);
if ( data.indexOf("<b>System error</b>") > -1 ) {
- var w;
- if ( w = window.open("about:blank") ) {
- w.document.write(data);
- } else {
- // popup blocking? should use an overlib popup instead
- alert("Error popup disabled; try disabling popup blocking to see");
- }
+ // trim this a little
+ var end = data.indexOf('<a href="#raw">') - 1;
+ data = data.substring(0, end);
+
+ overlib(data,
+ WIDTH, 480, MIDX, 0, MIDY, 0,
+ CAPTION, 'Error', STICKY, AUTOSTATUSCAP, DRAGGABLE,
+ CLOSECLICK, BGCOLOR, '#f00', CGCOLOR, '#f00'
+ );
+ //var w;
+ //if ( w = window.open("about:blank") ) {
+ // w.document.write(data);
+ //} else {
+ // // popup blocking? should use an overlib popup instead
+ // alert("Error popup disabled; try disabling popup blocking to see");
+ //}
+ } else {
+ // invoke the callback
+ a[a.length-1](data);
}
}
}
diff --git a/httemplate/misc/xmlhttp-mib-browse.html b/httemplate/misc/xmlhttp-mib-browse.html
new file mode 100644
index 0000000..f3084ff
--- /dev/null
+++ b/httemplate/misc/xmlhttp-mib-browse.html
@@ -0,0 +1,161 @@
+%#<% Data::Format::HTML->new->format($index{by_path}) %>
+% my $json = "JSON"->new->canonical;
+<% $json->encode($result) %>
+<%init>
+#<%once> #enable me in production
+use SNMP;
+SNMP::initMib();
+my $mib = \%SNMP::MIB;
+
+# make an index of the leaf nodes
+my %index = (
+ by_objectID => {}, # {.1.3.6.1.2.1.1.1}
+ by_fullname => {}, # {iso.org.dod.internet.mgmt.mib-2.system.sysDescr}
+ by_path => {}, # {iso}{org}{dod}{internet}{mgmt}{mib-2}{system}{sysDescr}
+ module => {}, #{SNMPv2-MIB}{by_path}{iso}{org}...
+ #{SNMPv2-MIB}{by_fullname}{iso.org...}
+);
+
+my %name_of_oid = (); # '.1.3.6.1' => 'iso.org.dod.internet'
+
+# build up path names
+my $fullname;
+$fullname = sub {
+ my $oid = shift;
+ return $name_of_oid{$oid} if exists $name_of_oid{$oid};
+
+ my $object = $mib->{$oid};
+ my $myname = '.' . $object->{label};
+ # cut off the last element and recurse
+ $oid =~ /^(\.[\d\.]+)?(\.\d+)$/;
+ if ( length($1) ) {
+ $myname = $fullname->($1) . $myname;
+ }
+ return $name_of_oid{$oid} = $myname
+};
+
+my @oids = keys(%$mib); # dotted numeric OIDs
+foreach my $oid (@oids) {
+ my $object = {};
+ %$object = %{ $mib->{$oid} }; # untie it
+ # and remove references
+ delete $object->{parent};
+ delete $object->{children};
+ delete $object->{nextNode};
+ $index{by_objectID}{$oid} = $object;
+ my $myname = $fullname->($oid);
+ $object->{fullname} = $myname;
+ $index{by_fullname}{$myname} = $object;
+ my $moduleID = $object->{moduleID};
+ $index{module}{$moduleID} ||= { by_fullname => {}, by_path => {} };
+ $index{module}{$moduleID}{by_fullname}{$myname} = $object;
+}
+my @names = sort {$a cmp $b} keys %{ $index{by_fullname} };
+foreach my $myname (@names) {
+ my $obj = $index{by_fullname}{$myname};
+ my $moduleID = $obj->{moduleID};
+ my @parts = split('\.', $myname);
+ shift @parts; # always starts with an empty string
+ for ($index{by_path}, $index{module}{$moduleID}{by_path}) {
+ my $subindex = $_;
+ for my $this_part (@parts) {
+ $subindex = $subindex->{$this_part} ||= {};
+ }
+ # $subindex now = $index{by_path}{foo}{bar}{baz}.
+ # set {''} = the object with that name.
+ # and set object $index{by_path}{foo}{bar}{baz}{''} =
+ # the object named .foo.bar.baz
+ $subindex->{''} = $obj;
+ }
+}
+
+#</%once>
+#<%init>
+# no ACL for this
+my $sub = $cgi->param('sub');
+my $result = {};
+if ( $sub eq 'search' ) {
+ warn "search: ".$cgi->param('arg')."\n";
+ my ($module, $string) = split(':', $cgi->param('arg'), 2);
+ my $idx; # the branch of the index to use for this search
+ if ( $module eq 'ANY' ) {
+ $idx = \%index;
+ } elsif (exists($index{module}{$module}) ) {
+ $idx = $index{module}{$module};
+ } else {
+ warn "unknown MIB moduleID: $module\n";
+ $idx = {}; # will return nothing, because you've somehow sent a bad moduleID
+ }
+ if ( exists($index{by_fullname}{$string}) ) {
+ warn "exact match\n";
+ # don't make this module-selective--if the path matches an existing
+ # object, return that object
+ %$result = %{ $index{by_fullname}{$string} }; # put the object info in $result
+ #warn Dumper $result;
+ }
+ my @choices; # menu options to return
+ if ( $string =~ /^[\.\d]+$/ ) {
+ # then this is a numeric path
+ # ignore the module filter, and return everything starting with $string
+ if ( $string =~ /^\./ ) {
+ @choices = grep /^\Q$string\E/, keys %{$index{by_objectID}};
+ } else {
+ # or everything containing it
+ @choices = grep /\Q$string\E/, keys %{$index{by_objectID}};
+ }
+ @choices = map { $index{by_objectID}{$_}->{fullname} } @choices;
+ } elsif ( $string eq '' or $string =~ /^\./ ) {
+ # then this is an absolute path
+ my @parts = split('\.', $string);
+ shift @parts;
+ my $subindex = $idx->{by_path};
+ my $path = '';
+ @choices = keys %$subindex;
+ # walk all the specified path parts
+ foreach my $this_part (@parts) {
+ # stop before walking off the map
+ last if !exists($subindex->{$this_part});
+ $subindex = $subindex->{$this_part};
+ $path .= '.' . $this_part;
+ @choices = grep {$_} keys %$subindex;
+ }
+ # skip uninteresting nodes: those that aren't accessible nodes (have no
+ # data type), and have only one path forward
+ while ( scalar(@choices) == 1
+ and (!exists $subindex->{''} or $subindex->{''}->{type} eq '') ) {
+
+ $subindex = $subindex->{ $choices[0] };
+ $path .= '.' . $choices[0];
+ @choices = grep {$_} keys %$subindex;
+
+ }
+
+ # if we are on an existing node, and the entered path didn't exactly
+ # match another node, return the current node as the result
+ if (!keys %$result and exists($subindex->{''})) {
+ %$result = %{ $subindex->{''} };
+ }
+ # prepend the path up to this point
+ foreach (@choices) {
+ $_ = $path.'.'.$_;
+ # also label accessible nodes for the UI
+ if ( exists($subindex->{$_}{''}) and $subindex->{$_}{''}{'type'} ) {
+ $_ .= '-';
+ }
+ }
+ # also include one level above the originally requested path,
+ # for tree-like navigation
+ if ( $string =~ /^(.+)\.[^\.]+/ ) {
+ unshift @choices, $1;
+ }
+ } else {
+ # then this is a full-text search
+ warn "/$string/\n";
+ @choices = grep /\Q$string\E/i, keys(%{ $idx->{by_fullname} });
+ }
+ @choices = sort @choices;
+ $result->{choices} = \@choices;
+} elsif ( $sub eq 'get_module_list' ) {
+ $result = { modules => [ sort keys(%{ $index{module} }) ] };
+}
+</%init>