[freeside-commits] branch master updated. b474400512cb725399e57d52383d0a0c407690b0

Mark Wells mark at 420.am
Fri Nov 30 16:26:25 PST 2012


The branch, master has been updated
       via  b474400512cb725399e57d52383d0a0c407690b0 (commit)
       via  a2a69f909cad813d7164bae805e87f5874a9fdae (commit)
      from  226fffec6fd0154ea8798b58321d4d119341879f (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit b474400512cb725399e57d52383d0a0c407690b0
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Nov 30 16:21:34 2012 -0800

    styling fixes, #16588

diff --git a/httemplate/edit/elements/part_export/broadband_snmp.html b/httemplate/edit/elements/part_export/broadband_snmp.html
index 8df0b8e..4c0367c 100644
--- a/httemplate/edit/elements/part_export/broadband_snmp.html
+++ b/httemplate/edit/elements/part_export/broadband_snmp.html
@@ -46,7 +46,7 @@ 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;
+  document.getElementById('datatype'+rownum).value = obj.type;
 }
 </script>
 
@@ -69,7 +69,7 @@ function receive_mib(obj, rownum) {
     <INPUT NAME="oid" ID="oid" SIZE="60" onclick="open_select_mib(this)">
   </TD>
   <TD>
-    <SPAN ID="datatype"></SPAN>
+    <INPUT TYPE="text" NAME="datatype" ID="datatype" READONLY=1>
   </TD>
   <TD>
     <INPUT NAME="value" ID="value">
@@ -77,10 +77,10 @@ function receive_mib(obj, rownum) {
 </TR>
 <& /elements/auto-table.html,
   template_row  => 'mytemplate',
-  fieldorder    => ['action', 'oid', 'value'],
+  fieldorder    => ['action', 'oid', 'datatype', 'value'],
   data          => \@data,
 &>
-<INPUT TYPE="hidden" NAME="multi_options" VALUE="action,oid,value">
+<INPUT TYPE="hidden" NAME="multi_options" VALUE="action,oid,datatype,value">
 <& foot.html, %opt &>
 <%init>
 my %opt = @_;
@@ -88,11 +88,12 @@ 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 @types   = split("\n", $part_export->option('datatype'));
 my @values  = split("\n", $part_export->option('value'));
 
 my @data;
 while (@actions or @oids or @values) {
-  my @thisrow = (shift(@actions), shift(@oids), shift(@values));
+  my @thisrow = (shift(@actions), shift(@oids), shift(@types), shift(@values));
   push @data, \@thisrow if grep length($_), @thisrow;
 }
 
diff --git a/httemplate/elements/auto-table.html b/httemplate/elements/auto-table.html
index ed01109..9aff94e 100644
--- a/httemplate/elements/auto-table.html
+++ b/httemplate/elements/auto-table.html
@@ -168,7 +168,6 @@ $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) {
diff --git a/httemplate/elements/select-mib-popup.html b/httemplate/elements/select-mib-popup.html
index f8e3ae3..bd485ef 100644
--- a/httemplate/elements/select-mib-popup.html
+++ b/httemplate/elements/select-mib-popup.html
@@ -1,12 +1,13 @@
 <& /elements/header-popup.html &>
+<DIV STYLE="visibility: hidden; position: absolute" ID="measurebox"></DIV>
 <TABLE WIDTH="100%">
 <TR>
-  <TD ALIGN="right">Module:</TD>
+  <TD WIDTH="30%" 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>
+  <TD><INPUT TYPE="text" NAME="path" ID="input_path" WIDTH="100%"></TD>
 </TR>
 <TR>
   <TD COLSPAN=2>
@@ -14,7 +15,7 @@
   </TD>
 </TR>
 <TR>
-  <TH COLSPAN=2 ID="mib_objectID"></TH>
+  <TH ALIGN="center" COLSPAN=2 ID="mib_objectID"></TH>
 </TR>
 <TR>
   <TD ALIGN="right">Module: </TD><TD ID="mib_moduleID"></TD>
@@ -40,7 +41,7 @@ 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';
+    state ? '' : 'none';
 }
 
 function clear_list() {
@@ -48,6 +49,7 @@ function clear_list() {
   select_path.options.length = 0;
 }
 
+var measurebox = document.getElementById('measurebox');
 function add_item(value) {
   var select_path = document.getElementById('select_path');
   var input_path = document.getElementById('input_path');
@@ -57,7 +59,21 @@ function add_item(value) {
     opt.className = 'leaf';
     v = v.substring(0, v.length - 1);
   }
-  opt.value = opt.text = v;
+  var optvalue = v; // may not be the name we display
+  // shorten these if they don't fit in the box
+  if ( v.length > 30 ) { // unless they're already really short
+    measurebox.innerHTML = v;
+    while ( measurebox.clientWidth > select_path.clientWidth - 10
+            && v.match(/^\..*\./) ) {
+      v = v.replace(/^\.[^\.]+/, '');
+      measurebox.innerHTML = v;
+    }
+    if ( optvalue != v ) {
+      v = '...' + v;
+    }
+  }
+  opt.value = optvalue;
+  opt.text = v;
   opt.selected = (input_path.value == v);
   select_path.add(opt, null);
 }

commit a2a69f909cad813d7164bae805e87f5874a9fdae
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Nov 29 22:03:29 2012 -0800

    broadband_snmp export: better MIB selection

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 %>: </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 %>: </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>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Mason.pm                                     |    1 +
 FS/FS/part_export.pm                               |   13 +
 FS/FS/part_export/broadband_snmp.pm                |  150 ++++++----
 httemplate/browse/part_export.cgi                  |   48 +++-
 httemplate/edit/cdr_type.cgi                       |   22 +-
 .../edit/elements/part_export/broadband_snmp.html  |  101 +++++++
 httemplate/edit/elements/part_export/foot.html     |    6 +
 httemplate/edit/elements/part_export/head.html     |   19 ++
 httemplate/edit/part_export.cgi                    |    9 +
 httemplate/edit/process/cdr_type.cgi               |    1 -
 httemplate/edit/process/part_export.cgi            |   29 ++-
 httemplate/edit/rate_time.cgi                      |   41 +++-
 httemplate/elements/auto-table.html                |  310 ++++++++++----------
 httemplate/elements/select-mib-popup.html          |  186 ++++++++++++
 httemplate/elements/xmlhttp.html                   |   32 ++-
 httemplate/misc/xmlhttp-mib-browse.html            |  161 ++++++++++
 16 files changed, 893 insertions(+), 236 deletions(-)
 create mode 100644 httemplate/edit/elements/part_export/broadband_snmp.html
 create mode 100644 httemplate/edit/elements/part_export/foot.html
 create mode 100644 httemplate/edit/elements/part_export/head.html
 create mode 100644 httemplate/elements/select-mib-popup.html
 create mode 100644 httemplate/misc/xmlhttp-mib-browse.html




More information about the freeside-commits mailing list