per-agent disable_previous_balance, #15863
[freeside.git] / FS / FS / svc_acct.pm
index 9236067..3bb7af4 100644 (file)
@@ -1,13 +1,19 @@
 package FS::svc_acct;
 
 use strict;
-use base qw( FS::svc_Domain_Mixin FS::svc_CGPRule_Mixin FS::svc_Common );
+use base qw( FS::svc_Domain_Mixin
+             FS::svc_CGP_Mixin
+             FS::svc_CGPRule_Mixin
+             FS::svc_Radius_Mixin
+             FS::svc_Tower_Mixin
+             FS::svc_Common );
 use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $dir_prefix @shells $usernamemin
              $usernamemax $passwordmin $passwordmax
              $username_ampersand $username_letter $username_letterfirst
              $username_noperiod $username_nounderscore $username_nodash
              $username_uppercase $username_percent $username_colon
+             $username_slash $username_equals $username_pound
              $password_noampersand $password_noexclamation
              $warning_template $warning_from $warning_subject $warning_mimetype
              $warning_cc
@@ -21,7 +27,7 @@ use Carp;
 use Fcntl qw(:flock);
 use Date::Format;
 use Crypt::PasswdMD5 1.2;
-use Digest::SHA1 'sha1_base64';
+use Digest::SHA 'sha1_base64';
 use Digest::MD5 'md5_base64';
 use Data::Dumper;
 use Text::Template;
@@ -41,11 +47,14 @@ use FS::svc_pbx;
 use FS::raddb;
 use FS::queue;
 use FS::radius_usergroup;
+use FS::radius_group;
 use FS::export_svc;
 use FS::part_export;
 use FS::svc_forward;
 use FS::svc_www;
 use FS::cdr;
+use FS::acct_snarf;
+use FS::tower_sector;
 
 $DEBUG = 0;
 $me = '[FS::svc_acct]';
@@ -72,6 +81,9 @@ FS::UID->install_callback( sub {
   $username_ampersand = $conf->exists('username-ampersand');
   $username_percent = $conf->exists('username-percent');
   $username_colon = $conf->exists('username-colon');
+  $username_slash = $conf->exists('username-slash');
+  $username_equals = $conf->exists('username-equals');
+  $username_pound = $conf->exists('username-pound');
   $password_noampersand = $conf->exists('password-noexclamation');
   $password_noexclamation = $conf->exists('password-noexclamation');
   $dirhash = $conf->config('dirhash') || 0;
@@ -99,7 +111,7 @@ FS::UID->install_callback( sub {
 );
 
 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
+@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '.', ',' );
 
 sub _cache {
   my $self = shift;
@@ -244,6 +256,7 @@ sub table_info {
     'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
     'display_weight' => 10,
     'cancel_weight'  => 50, 
+    'ip_field' => 'slipip',
     'fields' => {
         'dir'       => 'Home directory',
         'uid'       => {
@@ -327,11 +340,13 @@ sub table_info {
                          disable_inventory => 1,
                          disable_select => 1, #UI wonky, pry works otherwise
                        },
+        'sectornum' => 'Tower sector',
         'usergroup' => {
                          label => 'RADIUS groups',
-                         type  => 'radius_usergroup_selector',
+                         type  => 'select-radius_group.html',
                          disable_inventory => 1,
                          disable_select => 1,
+                         multiple => 1,
                        },
         'seconds'   => { label => 'Seconds',
                          label_sort => 'with Time Remaining',
@@ -439,7 +454,28 @@ sub table_info {
         'cgp_addmailtrailer' => { label => 'Add trailer to sent mail',
                                   type  => 'checkbox',
                                 },
-        #XXX archive messages, mailing lists
+        'cgp_archiveafter'   => {
+          label       => 'Archive messages after',
+          type        => 'select',
+          select_hash => [ 
+                           -2 => 'default(730 days)',
+                           0 => 'Never',
+                           86400 => '24 hours',
+                           172800 => '2 days',
+                           259200 => '3 days',
+                           432000 => '5 days',
+                           604800 => '7 days',
+                           1209600 => '2 weeks',
+                           2592000 => '30 days',
+                           7776000 => '90 days',
+                           15552000 => '180 days',
+                           31536000 => '365 days',
+                           63072000 => '730 days',
+                         ],
+          disable_inventory => 1,
+          disable_select    => 1,
+        },
+        #XXX mailing lists
 
         #preferences
         'cgp_deletemode' => { 
@@ -450,8 +486,9 @@ sub table_info {
                               disable_select    => 1,
                             },
         'cgp_emptytrash' => { 
-                              label => 'Communigate on logout remove trash',
-                              type  => 'text',
+                              label     => 'Communigate on logout remove trash',
+                              type        => 'select',
+                              select_list => __PACKAGE__->cgp_emptytrash_values,
                               disable_inventory => 1,
                               disable_select    => 1,
                             },
@@ -463,71 +500,9 @@ sub table_info {
                             disable_select    => 1,
                           },
         'cgp_timezone' => {
-                            label => 'Communigate time zone',
-                            type  => 'select',
-                            select_list => [ '',
-                                             'HostOS',
-                                             '(+0100) Algeria/Congo',
-                                             '(+0200) Egypt/South Africa',
-                                             '(+0300) Saudi Arabia',
-                                             '(+0400) Oman',
-                                             '(+0500) Pakistan',
-                                             '(+0600) Bangladesh',
-                                             '(+0700) Thailand/Vietnam',
-                                             '(+0800) China/Malaysia',
-                                             '(+0900) Japan/Korea',
-                                             '(+1000) Queensland',
-                                             '(+1100) Micronesia',
-                                             '(+1200) Fiji',
-                                             '(+1300) Tonga/Kiribati',
-                                             '(+1400) Christmas Islands',
-                                             '(-0100) Azores/Cape Verde',
-                                             '(-0200) Fernando de Noronha',
-                                             '(-0300) Argentina/Uruguay',
-                                             '(-0400) Venezuela/Guyana',
-                                             '(-0500) Haiti/Peru',
-                                             '(-0600) Central America',
-                                             '(-0700) Arisona',
-                                             '(-0800) Adamstown',
-                                             '(-0900) Marquesas Islands',
-                                             '(-1000) Hawaii/Tahiti',
-                                             '(-1100) Samoa',
-                                             'Asia/Afghanistan',
-                                             'Asia/India',
-                                             'Asia/Iran',
-                                             'Asia/Iraq',
-                                             'Asia/Israel',
-                                             'Asia/Jordan',
-                                             'Asia/Lebanon',
-                                             'Asia/Syria',
-                                             'Australia/Adelaide',
-                                             'Australia/East',
-                                             'Australia/NorthernTerritory',
-                                             'Europe/Central',
-                                             'Europe/Eastern',
-                                             'Europe/Moscow',
-                                             'Europe/Western',
-                                             'GMT (+0000)',
-                                             'Newfoundland',
-                                             'NewZealand/Auckland',
-                                             'NorthAmerica/Alaska',
-                                             'NorthAmerica/Atlantic',
-                                             'NorthAmerica/Central',
-                                             'NorthAmerica/Eastern',
-                                             'NorthAmerica/Mountain',
-                                             'NorthAmerica/Pacific',
-                                             'Russia/Ekaterinburg',
-                                             'Russia/Irkutsk',
-                                             'Russia/Kamchatka',
-                                             'Russia/Krasnoyarsk',
-                                             'Russia/Magadan',
-                                             'Russia/Novosibirsk',
-                                             'Russia/Vladivostok',
-                                             'Russia/Yakutsk',
-                                             'SouthAmerica/Brasil',
-                                             'SouthAmerica/Chile',
-                                             'SouthAmerica/Paraguay',
-                                           ],
+                            label       => 'Communigate time zone',
+                            type        => 'select',
+                            select_list => __PACKAGE__->cgp_timezone_values,
                             disable_inventory => 1,
                             disable_select    => 1,
                           },
@@ -554,7 +529,6 @@ sub table_info {
         },
 
         #mail
-        #XXX vacation message, redirect all mail, mail rules
         #XXX RPOP settings
 
     },
@@ -565,22 +539,6 @@ sub table { 'svc_acct'; }
 
 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
 
-sub _fieldhandlers {
-  {
-    #false laziness with edit/svc_acct.cgi
-    'usergroup' => sub { 
-                         my( $self, $groups ) = @_;
-                         if ( ref($groups) eq 'ARRAY' ) {
-                           $groups;
-                         } elsif ( length($groups) ) {
-                           [ split(/\s*,\s*/, $groups) ];
-                         } else {
-                           [];
-                         }
-                       },
-  };
-}
-
 sub last_login {
   shift->_lastlog('in', @_);
 }
@@ -733,7 +691,7 @@ sub insert {
   my $dbh = dbh;
 
   my @jobnums;
-  my $error = $self->SUPER::insert(
+  my $error = $self->SUPER::insert( # usergroup is here
     'jobnums'       => \@jobnums,
     'child_objects' => $self->child_objects,
     %options,
@@ -743,20 +701,6 @@ sub insert {
     return $error;
   }
 
-  if ( $self->usergroup ) {
-    foreach my $groupname ( @{$self->usergroup} ) {
-      my $radius_usergroup = new FS::radius_usergroup ( {
-        svcnum    => $self->svcnum,
-        groupname => $groupname,
-      } );
-      my $error = $radius_usergroup->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return $error;
-      }
-    }
-  }
-
   unless ( $skip_fuzzyfiles ) {
     $error = $self->queue_fuzzyfiles_update;
     if ( $error ) {
@@ -781,82 +725,93 @@ sub insert {
     }
 
     #welcome email
-    my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
-      = ('','','','','','');
-
-    if ( $conf->exists('welcome_email', $agentnum) ) {
-      $welcome_template = new Text::Template (
-        TYPE   => 'ARRAY',
-        SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
-      ) or warn "can't create welcome email template: $Text::Template::ERROR";
-      $welcome_from = $conf->config('welcome_email-from', $agentnum);
-        # || 'your-isp-is-dum'
-      $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
-        || 'Welcome';
-      $welcome_subject_template = new Text::Template (
-        TYPE   => 'STRING',
-        SOURCE => $welcome_subject,
-      ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
-      $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
-        || 'text/plain';
-    }
-    if ( $welcome_template && $cust_pkg ) {
-      my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
-      if ( $to ) {
-
-        my %hash = (
-                     'custnum'  => $self->custnum,
-                     'username' => $self->username,
-                     'password' => $self->_password,
-                     'first'    => $cust_main->first,
-                     'last'     => $cust_main->getfield('last'),
-                     'pkg'      => $cust_pkg->part_pkg->pkg,
-                   );
-        my $wqueue = new FS::queue {
-          'svcnum' => $self->svcnum,
-          'job'    => 'FS::svc_acct::send_email'
-        };
-        my $error = $wqueue->insert(
-          'to'       => $to,
-          'from'     => $welcome_from,
-          'subject'  => $welcome_subject_template->fill_in( HASH => \%hash, ),
-          'mimetype' => $welcome_mimetype,
-          'body'     => $welcome_template->fill_in( HASH => \%hash, ),
-        );
-        if ( $error ) {
-          $dbh->rollback if $oldAutoCommit;
-          return "error queuing welcome email: $error";
+    my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude');
+    unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) {
+        my $error = '';
+        my $msgnum = $conf->config('welcome_msgnum', $agentnum);
+        if ( $msgnum ) {
+          my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
+          $error = $msg_template->send('cust_main' => $cust_main,
+                                       'object'    => $self);
         }
-
-        if ( $options{'depend_jobnum'} ) {
-          warn "$me depend_jobnum found; adding to welcome email dependancies"
-            if $DEBUG;
-          if ( ref($options{'depend_jobnum'}) ) {
-            warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
-                 "to welcome email dependancies"
-              if $DEBUG;
-            push @jobnums, @{ $options{'depend_jobnum'} };
-          } else {
-            warn "$me adding job $options{'depend_jobnum'} ".
-                 "to welcome email dependancies"
-              if $DEBUG;
-            push @jobnums, $options{'depend_jobnum'};
+        else { #!$msgnum
+          my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
+            = ('','','','','','');
+
+          if ( $conf->exists('welcome_email', $agentnum) ) {
+            $welcome_template = new Text::Template (
+              TYPE   => 'ARRAY',
+              SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
+            ) or warn "can't create welcome email template: $Text::Template::ERROR";
+            $welcome_from = $conf->config('welcome_email-from', $agentnum);
+              # || 'your-isp-is-dum'
+            $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
+              || 'Welcome';
+            $welcome_subject_template = new Text::Template (
+              TYPE   => 'STRING',
+              SOURCE => $welcome_subject,
+            ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
+            $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
+              || 'text/plain';
           }
-        }
-
-        foreach my $jobnum ( @jobnums ) {
-          my $error = $wqueue->depend_insert($jobnum);
-          if ( $error ) {
-            $dbh->rollback if $oldAutoCommit;
-            return "error queuing welcome email job dependancy: $error";
-          }
-        }
-
-      }
-
+          if ( $welcome_template ) {
+            my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
+            if ( $to ) {
+
+              my %hash = (
+                           'custnum'  => $self->custnum,
+                           'username' => $self->username,
+                           'password' => $self->_password,
+                           'first'    => $cust_main->first,
+                           'last'     => $cust_main->getfield('last'),
+                           'pkg'      => $cust_pkg->part_pkg->pkg,
+                         );
+              my $wqueue = new FS::queue {
+                'svcnum' => $self->svcnum,
+                'job'    => 'FS::svc_acct::send_email'
+              };
+              my $error = $wqueue->insert(
+                'to'       => $to,
+                'from'     => $welcome_from,
+                'subject'  => $welcome_subject_template->fill_in( HASH => \%hash, ),
+                'mimetype' => $welcome_mimetype,
+                'body'     => $welcome_template->fill_in( HASH => \%hash, ),
+              );
+              if ( $error ) {
+                $dbh->rollback if $oldAutoCommit;
+                return "error queuing welcome email: $error";
+              }
+
+              if ( $options{'depend_jobnum'} ) {
+                warn "$me depend_jobnum found; adding to welcome email dependancies"
+                  if $DEBUG;
+                if ( ref($options{'depend_jobnum'}) ) {
+                  warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
+                       "to welcome email dependancies"
+                    if $DEBUG;
+                  push @jobnums, @{ $options{'depend_jobnum'} };
+                } else {
+                  warn "$me adding job $options{'depend_jobnum'} ".
+                       "to welcome email dependancies"
+                    if $DEBUG;
+                  push @jobnums, $options{'depend_jobnum'};
+                }
+              }
+
+              foreach my $jobnum ( @jobnums ) {
+                my $error = $wqueue->depend_insert($jobnum);
+                if ( $error ) {
+                  $dbh->rollback if $oldAutoCommit;
+                  return "error queuing welcome email job dependancy: $error";
+                }
+              }
+
+            }
+
+          } # if $welcome_template
+        } # if !$msgnum
     }
-
-  } # if ( $cust_pkg )
+  } # if $cust_pkg
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
@@ -958,22 +913,12 @@ sub delete {
     }
   }
 
-  my $error = $self->SUPER::delete;
+  my $error = $self->SUPER::delete; # usergroup here
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
-  foreach my $radius_usergroup (
-    qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
-  ) {
-    my $error = $radius_usergroup->delete;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
-
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 }
@@ -1016,6 +961,10 @@ sub replace {
 
   }
 
+  return "can't change username"
+    if $old->username ne $new->username
+    && $conf->exists('svc_acct-no_edit_username');
+
   #change homdir when we change username
   $new->setfield('dir', '') if $old->username ne $new->username;
 
@@ -1030,49 +979,7 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  # redundant, but so $new->usergroup gets set
-  $error = $new->check;
-  return $error if $error;
-
-  $old->usergroup( [ $old->radius_groups ] );
-  if ( $DEBUG ) {
-    warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
-    warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
-  }
-  if ( $new->usergroup ) {
-    #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
-    my @newgroups = @{$new->usergroup};
-    foreach my $oldgroup ( @{$old->usergroup} ) {
-      if ( grep { $oldgroup eq $_ } @newgroups ) {
-        @newgroups = grep { $oldgroup ne $_ } @newgroups;
-        next;
-      }
-      my $radius_usergroup = qsearchs('radius_usergroup', {
-        svcnum    => $old->svcnum,
-        groupname => $oldgroup,
-      } );
-      my $error = $radius_usergroup->delete;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error deleting radius_usergroup $oldgroup: $error";
-      }
-    }
-
-    foreach my $newgroup ( @newgroups ) {
-      my $radius_usergroup = new FS::radius_usergroup ( {
-        svcnum    => $new->svcnum,
-        groupname => $newgroup,
-      } );
-      my $error = $radius_usergroup->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error adding radius_usergroup $newgroup: $error";
-      }
-    }
-
-  }
-
-  $error = $new->SUPER::replace($old, @_);
+  $error = $new->SUPER::replace($old, @_); # usergroup here
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error if $error;
@@ -1210,45 +1117,47 @@ sub check {
 
   my($recref) = $self->hashref;
 
-  my $x = $self->setfixed( $self->_fieldhandlers );
+  my $x = $self->setfixed;
   return $x unless ref($x);
   my $part_svc = $x;
 
-  if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
-    $self->usergroup(
-      [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
-  }
-
   my $error = $self->ut_numbern('svcnum')
               #|| $self->ut_number('domsvc')
               || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
               || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx',    'svcnum' )
+              || $self->ut_foreign_keyn('sectornum','tower_sector','sectornum')
               || $self->ut_textn('sec_phrase')
               || $self->ut_snumbern('seconds')
               || $self->ut_snumbern('upbytes')
               || $self->ut_snumbern('downbytes')
               || $self->ut_snumbern('totalbytes')
+              || $self->ut_snumbern('seconds_threshold')
+              || $self->ut_snumbern('upbytes_threshold')
+              || $self->ut_snumbern('downbytes_threshold')
+              || $self->ut_snumbern('totalbytes_threshold')
               || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
               || $self->ut_enum('password_selfchange', [ '', 'Y' ])
               || $self->ut_enum('password_recover',    [ '', 'Y' ])
+              #cardfortress
+              || $self->ut_anything('cf_privatekey')
+              #communigate
               || $self->ut_textn('cgp_accessmodes')
               || $self->ut_alphan('cgp_type')
               || $self->ut_textn('cgp_aliases' ) #well
-              #settings
+              # settings
               || $self->ut_alphasn('cgp_rulesallowed')
               || $self->ut_enum('cgp_rpopallowed', [ '', 'Y' ])
               || $self->ut_enum('cgp_mailtoall', [ '', 'Y' ])
               || $self->ut_enum('cgp_addmailtrailer', [ '', 'Y' ])
-              #preferences
+              || $self->ut_snumbern('cgp_archiveafter')
+              # preferences
               || $self->ut_alphasn('cgp_deletemode')
-              || $self->ut_alphan('cgp_emptytrash')
+              || $self->ut_enum('cgp_emptytrash', $self->cgp_emptytrash_values)
               || $self->ut_alphan('cgp_language')
               || $self->ut_textn('cgp_timezone')
               || $self->ut_textn('cgp_skinname')
               || $self->ut_textn('cgp_prontoskinname')
               || $self->ut_alphan('cgp_sendmdnmode')
-              #XXX vacation message, redirect all mail, mail rules
-              #XXX RPOP settings
   ;
   return $error if $error;
 
@@ -1268,16 +1177,14 @@ sub check {
   }
 
   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
-  if ( $username_uppercase ) {
-    $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
-      or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
-    $recref->{username} = $1;
-  } else {
-    $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
-      or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
-    $recref->{username} = $1;
-  }
 
+  $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#]{$usernamemin,$ulen})$/i
+    or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
+  $recref->{username} = $1;
+
+  unless ( $username_uppercase ) {
+    $recref->{username} =~ /[A-Z]/ and return gettext('illegal_username');
+  }
   if ( $username_letterfirst ) {
     $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
   } elsif ( $username_letter ) {
@@ -1301,6 +1208,16 @@ sub check {
   unless ( $username_colon ) {
     $recref->{username} =~ /\:/ and return gettext('illegal_username');
   }
+  unless ( $username_slash ) {
+    $recref->{username} =~ /\// and return gettext('illegal_username');
+  }
+  unless ( $username_equals ) {
+    $recref->{username} =~ /\=/ and return gettext('illegal_username');
+  }
+  unless ( $username_pound ) {
+    $recref->{username} =~ /\#/ and return gettext('illegal_username');
+  }
+
 
   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
   $recref->{popnum} = $1;
@@ -1344,7 +1261,7 @@ sub check {
 
   unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
 
-    $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
+    $recref->{dir} =~ /^([\/\w\-\.\&\:\#]*)$/
       or return "Illegal directory: ". $recref->{dir};
     $recref->{dir} = $1;
     return "Illegal directory"
@@ -1379,8 +1296,7 @@ sub check {
       $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
     }
   }
-  $self->getfield('finger') =~
-    /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
+  $self->getfield('finger') =~ /^([\w \,\.\-\'\&\t\!\@\#\$\%\(\)\+\;\"\?\/\*\<\>]+)$/
       or return "Illegal finger: ". $self->getfield('finger');
   $self->setfield('finger', $1);
 
@@ -1600,6 +1516,8 @@ sub set_password {
       $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
     }
     # else $encryption eq 'plain', do nothing
+    $pass .= '=' x (4 - length($pass) % 4) #properly padded base64
+      if $encryption eq 'md5' || $encryption eq 'sha1';
     $pass = '{'.uc($encryption).'}'.$pass;
   }
   # else encoding eq 'plain'
@@ -1958,17 +1876,27 @@ sub email {
 =item acct_snarf
 
 Returns an array of FS::acct_snarf records associated with the account.
-If the acct_snarf table does not exist or there are no associated records,
-an empty list is returned
 
 =cut
 
 sub acct_snarf {
   my $self = shift;
-  return () unless dbdef->table('acct_snarf');
-  eval "use FS::acct_snarf;";
-  die $@ if $@;
-  qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
+  qsearch({
+    'table'    => 'acct_snarf',
+    'hashref'  => { 'svcnum' => $self->svcnum },
+    #'order_by' => 'ORDER BY priority ASC',
+  });
+}
+
+=item cgp_rpop_hashref
+
+Returns an arrayref of RPOP data suitable for Communigate Pro API commands.
+
+=cut
+
+sub cgp_rpop_hashref {
+  my $self = shift;
+  { map { $_->snarfname => $_->cgp_hashref } $self->acct_snarf };
 }
 
 =item decrement_upbytes OCTETS
@@ -2221,20 +2149,19 @@ sub _op_overlimit {
 
   my $cust_pkg = $self->cust_svc->cust_pkg;
 
-  my $conf_overlimit =
+  my @conf_overlimit =
     $cust_pkg
       ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
       : $conf->config('overlimit_groups');
 
   foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
 
-    my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
-    next unless $groups;
-
-    my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
+    my @groups = scalar(@conf_overlimit) ? @conf_overlimit
+                                         : split(' ',$part_export->option('overlimit_groups'));
+    next unless scalar(@groups);
 
     my $other = new FS::svc_acct $self->hashref;
-    $other->usergroup( $gref );
+    $other->usergroup(\@groups);
 
     my($new,$old);
     if ($action eq 'suspend') {
@@ -2283,7 +2210,7 @@ sub set_usage {
   my $reset = 0;
   my %handyhash = ();
   if ( $options{null} ) { 
-    %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
+    %handyhash = ( map { ( $_ => undef, $_."_threshold" => undef ) }
                    qw( seconds upbytes downbytes totalbytes )
                  );
   }
@@ -2305,7 +2232,7 @@ sub set_usage {
   #die $error if $error;         #services not explicity changed via the UI
 
   my $sql = "UPDATE svc_acct SET " .
-    join (',', map { "$_ =  $handyhash{$_}" } (keys %handyhash) ).
+    join (',', map { "$_ =  ?" } (keys %handyhash) ).
     " WHERE svcnum = ". $self->svcnum;
 
   warn "$me $sql\n"
@@ -2314,7 +2241,7 @@ sub set_usage {
   if (scalar(keys %handyhash)) {
     my $sth = $dbh->prepare( $sql )
       or die "Error preparing $sql: ". $dbh->errstr;
-    my $rv = $sth->execute();
+    my $rv = $sth->execute(values %handyhash);
     die "Error executing $sql: ". $sth->errstr
       unless defined($rv);
     die "Can't update usage for svcnum ". $self->svcnum
@@ -2544,25 +2471,7 @@ sub get_cdrs {
 
 }
 
-=item radius_groups
-
-Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
-
-=cut
-
-sub radius_groups {
-  my $self = shift;
-  if ( $self->usergroup ) {
-    confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
-      unless ref($self->usergroup) eq 'ARRAY';
-    #when provisioning records, export callback runs in svc_Common.pm before
-    #radius_usergroup records can be inserted...
-    @{$self->usergroup};
-  } else {
-    map { $_->groupname }
-      qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
-  }
-}
+# sub radius_groups has moved to svc_Radius_Mixin
 
 =item clone_suspended
 
@@ -2674,12 +2583,12 @@ sub crypt_password {
 
       my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
       if ( $encryption eq 'crypt' ) {
-        crypt(
+        return crypt(
           $self->_password,
           $saltset[int(rand(64))].$saltset[int(rand(64))]
         );
       } elsif ( $encryption eq 'md5' ) {
-        unix_md5_crypt( $self->_password );
+        return unix_md5_crypt( $self->_password );
       } elsif ( $encryption eq 'blowfish' ) {
         croak "unknown encryption method $encryption";
       } else {
@@ -2687,7 +2596,7 @@ sub crypt_password {
       }
 
     } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
-      $1;
+      return $1;
     }
 
   } elsif ( $self->_password_encoding eq 'crypt' ) {
@@ -2700,12 +2609,16 @@ sub crypt_password {
 
     my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
     if ( $encryption eq 'crypt' ) {
-      crypt(
+      return crypt(
         $self->_password,
         $saltset[int(rand(64))].$saltset[int(rand(64))]
       );
     } elsif ( $encryption eq 'md5' ) {
-      unix_md5_crypt( $self->_password );
+      return unix_md5_crypt( $self->_password );
+    } elsif ( $encryption eq 'sha1_base64' ) { #for acct_sql
+      my $pass = sha1_base64( $self->_password );
+      $pass .= '=' x (4 - length($pass) % 4); #properly padded base64
+      return $pass;
     } elsif ( $encryption eq 'blowfish' ) {
       croak "unknown encryption method $encryption";
     } else {
@@ -2726,12 +2639,12 @@ sub crypt_password {
 
       my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
       if ( $encryption eq 'crypt' ) {
-        crypt(
+        return crypt(
           $self->_password,
           $saltset[int(rand(64))].$saltset[int(rand(64))]
         );
       } elsif ( $encryption eq 'md5' ) {
-        unix_md5_crypt( $self->_password );
+        return unix_md5_crypt( $self->_password );
       } elsif ( $encryption eq 'blowfish' ) {
         croak "unknown encryption method $encryption";
       } else {
@@ -2908,7 +2821,7 @@ sub search {
 
   #agentnum
   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "agentnum = $1";
+    push @where, "cust_main.agentnum = $1";
   }
 
   #custnum
@@ -3097,56 +3010,6 @@ sub append_fuzzyfiles {
 }
 
 
-
-=item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
-
-=cut
-
-sub radius_usergroup_selector {
-  my $sel_groups = shift;
-  my %sel_groups = map { $_=>1 } @$sel_groups;
-
-  my $selectname = shift || 'radius_usergroup';
-
-  my $dbh = dbh;
-  my $sth = $dbh->prepare(
-    'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
-  ) or die $dbh->errstr;
-  $sth->execute() or die $sth->errstr;
-  my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
-
-  my $html = <<END;
-    <SCRIPT>
-    function ${selectname}_doadd(object) {
-      var myvalue = object.${selectname}_add.value;
-      var optionName = new Option(myvalue,myvalue,false,true);
-      var length = object.$selectname.length;
-      object.$selectname.options[length] = optionName;
-      object.${selectname}_add.value = "";
-    }
-    </SCRIPT>
-    <SELECT MULTIPLE NAME="$selectname">
-END
-
-  foreach my $group ( @all_groups ) {
-    $html .= qq(<OPTION VALUE="$group");
-    if ( $sel_groups{$group} ) {
-      $html .= ' SELECTED';
-      $sel_groups{$group} = 0;
-    }
-    $html .= ">$group</OPTION>\n";
-  }
-  foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
-    $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
-  };
-  $html .= '</SELECT>';
-
-  $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
-           qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
-
-  $html;
-}
-
 =item reached_threshold
 
 Performs some activities when svc_acct thresholds (such as number of seconds
@@ -3236,9 +3099,6 @@ The suspend, unsuspend and cancel methods update the database, but not the
 current object.  This is probably a bug as it's unexpected and
 counterintuitive.
 
-radius_usergroup_selector?  putting web ui components in here?  they should
-probably live somewhere else...
-
 insertion of RADIUS group stuff in insert could be done with child_objects now
 (would probably clean up export of them too)