export negative byte values to chillispot attributes as 0, RT#5815
[freeside.git] / FS / FS / svc_acct.pm
index 9fb72d1..d6d132b 100644 (file)
@@ -6,7 +6,7 @@ use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
              $usernamemax $passwordmin $passwordmax
              $username_ampersand $username_letter $username_letterfirst
              $username_noperiod $username_nounderscore $username_nodash
-             $username_uppercase $username_percent
+             $username_uppercase $username_percent $username_colon
              $password_noampersand $password_noexclamation
              $welcome_template $welcome_from
              $welcome_subject $welcome_subject_template $welcome_mimetype
@@ -16,16 +16,19 @@ use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
              $radius_password $radius_ip
              $dirhash
              @saltset @pw_set );
+use Math::BigInt;
 use Carp;
 use Fcntl qw(:flock);
 use Date::Format;
 use Crypt::PasswdMD5 1.2;
 use Data::Dumper;
+use Text::Template;
 use FS::UID qw( datasrc driver_name );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
 use FS::Msgcat qw(gettext);
 use FS::UI::bytecount;
+use FS::part_pkg;
 use FS::svc_Common;
 use FS::cust_svc;
 use FS::part_svc;
@@ -63,6 +66,7 @@ $FS::UID::callback{'FS::svc_acct'} = sub {
   $username_uppercase = $conf->exists('username-uppercase');
   $username_ampersand = $conf->exists('username-ampersand');
   $username_percent = $conf->exists('username-percent');
+  $username_colon = $conf->exists('username-colon');
   $password_noampersand = $conf->exists('password-noexclamation');
   $password_noexclamation = $conf->exists('password-noexclamation');
   $dirhash = $conf->config('dirhash') || 0;
@@ -269,7 +273,7 @@ sub table_info {
                          disable_inventory => 1,
                          disable_select => 1,
                        },
-        'finger'    => 'Real name (GECOS)',
+        'finger'    => 'Real name', # (GECOS)',
         'domsvc'    => {
                          label     => 'Domain',
                          #def_label => 'svcnum from svc_domain',
@@ -290,6 +294,7 @@ sub table_info {
                          type  => 'text',
                          disable_inventory => 1,
                          disable_select => 1,
+                         disable_part_svc_column => 1,
                        },
         'upbytes'   => { label => 'Upload',
                          type  => 'text',
@@ -297,6 +302,7 @@ sub table_info {
                          disable_select => 1,
                          'format' => \&FS::UI::bytecount::display_bytecount,
                          'parse' => \&FS::UI::bytecount::parse_bytecount,
+                         disable_part_svc_column => 1,
                        },
         'downbytes' => { label => 'Download',
                          type  => 'text',
@@ -304,6 +310,7 @@ sub table_info {
                          disable_select => 1,
                          'format' => \&FS::UI::bytecount::display_bytecount,
                          'parse' => \&FS::UI::bytecount::parse_bytecount,
+                         disable_part_svc_column => 1,
                        },
         'totalbytes'=> { label => 'Total up and download',
                          type  => 'text',
@@ -311,11 +318,13 @@ sub table_info {
                          disable_select => 1,
                          'format' => \&FS::UI::bytecount::display_bytecount,
                          'parse' => \&FS::UI::bytecount::parse_bytecount,
+                         disable_part_svc_column => 1,
                        },
         'seconds_threshold'   => { label => 'Seconds threshold',
                                    type  => 'text',
                                    disable_inventory => 1,
                                    disable_select => 1,
+                                   disable_part_svc_column => 1,
                                  },
         'upbytes_threshold'   => { label => 'Upload threshold',
                                    type  => 'text',
@@ -323,6 +332,7 @@ sub table_info {
                                    disable_select => 1,
                                    'format' => \&FS::UI::bytecount::display_bytecount,
                                    'parse' => \&FS::UI::bytecount::parse_bytecount,
+                                   disable_part_svc_column => 1,
                                  },
         'downbytes_threshold' => { label => 'Download threshold',
                                    type  => 'text',
@@ -330,6 +340,7 @@ sub table_info {
                                    disable_select => 1,
                                    'format' => \&FS::UI::bytecount::display_bytecount,
                                    'parse' => \&FS::UI::bytecount::parse_bytecount,
+                                   disable_part_svc_column => 1,
                                  },
         'totalbytes_threshold'=> { label => 'Total up and download threshold',
                                    type  => 'text',
@@ -337,6 +348,7 @@ sub table_info {
                                    disable_select => 1,
                                    'format' => \&FS::UI::bytecount::display_bytecount,
                                    'parse' => \&FS::UI::bytecount::parse_bytecount,
+                                   disable_part_svc_column => 1,
                                  },
         'last_login'=>           {
                                    label     => 'Last login',
@@ -448,8 +460,26 @@ sub label {
   $self->email(@_);
 }
 
+=item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns a longer string label for this acccount ("Real Name <username@domain>"
+if available, or "username@domain").
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
 =cut
 
+sub label_long {
+  my $self = shift;
+  my $label = $self->label(@_);
+  my $finger = $self->finger;
+  return $label unless $finger =~ /\S/;
+  my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
+  $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
+  "$finger <$label>";
+}
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this account to the database.  If there is an error, returns the error,
@@ -520,6 +550,27 @@ sub insert {
     return $error;
   }
 
+  # set usage fields and thresholds if unset but set in a package def
+  if ( $self->pkgnum ) {
+    my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+    my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
+    if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
+
+      my %values = $part_pkg->usage_valuehash;
+      my $multiplier = $conf->exists('svc_acct-usage_threshold') 
+                         ? 1 - $conf->config('svc_acct-usage_threshold')/100
+                         : 0.20; #doesn't matter
+
+      foreach ( keys %values ) {
+        next if $self->getfield($_);
+        $self->setfield( $_, $values{$_} );
+        $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
+          if $conf->exists('svc_acct-usage_threshold');
+      }
+
+    }
+  }
+
   my @jobnums;
   $error = $self->SUPER::insert(
     'jobnums'       => \@jobnums,
@@ -981,11 +1032,11 @@ sub check {
 
   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
   if ( $username_uppercase ) {
-    $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
+    $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})$/
+    $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
       or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
     $recref->{username} = $1;
   }
@@ -1016,6 +1067,9 @@ sub check {
   unless ( $username_percent ) {
     $recref->{username} =~ /\%/ and return gettext('illegal_username');
   }
+  unless ( $username_colon ) {
+    $recref->{username} =~ /\:/ and return gettext('illegal_username');
+  }
 
   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
   $recref->{popnum} = $1;
@@ -1263,7 +1317,8 @@ sub _check_duplicate {
     foreach my $dup_user ( @dup_user ) {
       my $dup_svcpart = $dup_user->cust_svc->svcpart;
       if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
-        return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
+        return "duplicate username ". $self->username.
+               ": conflicts with svcnum ". $dup_user->svcnum.
                " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
       }
     }
@@ -1271,9 +1326,9 @@ sub _check_duplicate {
     foreach my $dup_userdomain ( @dup_userdomain ) {
       my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
       if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
-        return "duplicate username\@domain: conflicts with svcnum ".
-               $dup_userdomain->svcnum. " via exportnum ".
-               $conflict_userdomain_svcpart{$dup_svcpart};
+        return "duplicate username\@domain ". $self->email.
+               ": conflicts with svcnum ". $dup_userdomain->svcnum.
+               " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
       }
     }
 
@@ -1281,9 +1336,11 @@ sub _check_duplicate {
       my $dup_svcpart = $dup_uid->cust_svc->svcpart;
       if ( exists($conflict_user_svcpart{$dup_svcpart})
            || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
-        return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
-               " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
-                                 || $conflict_userdomain_svcpart{$dup_svcpart};
+        return "duplicate uid ". $self->uid.
+               ": conflicts with svcnum ". $dup_uid->svcnum.
+               " via exportnum ".
+               ( $conflict_user_svcpart{$dup_svcpart}
+                 || $conflict_userdomain_svcpart{$dup_svcpart} );
       }
     }
 
@@ -1337,6 +1394,29 @@ sub radius_reply {
     $reply{'Session-Timeout'} = $self->seconds;
   }
 
+  if ( $conf->exists('radius-chillispot-max') ) {
+    #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
+
+    #hmm.  just because sqlradius.pm says so?
+    my %whatis = (
+      'input'  => 'up',
+      'output' => 'down',
+      'total'  => 'total',
+    );
+
+    foreach my $what (qw( input output total )) {
+      my $is = $whatis{$what}.'bytes';
+      if ( $self->$is() =~ /\d/ ) {
+        my $big = new Math::BigInt $self->$is();
+        $big = new Math::BigInto '0' if $big->is_neg();
+        my $att = "Chillispot-Max-\u$what";
+        $reply{"$att-Octets"}    = $big->copy->band(0xffffffff)->bstr;
+        $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
+      }
+    }
+
+  }
+
   %reply;
 }
 
@@ -1369,11 +1449,15 @@ sub radius_check {
   my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';  $check{$pw_attrib} = $password;
 
   my $cust_svc = $self->cust_svc;
-  die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
-    unless $cust_svc;
-  my $cust_pkg = $cust_svc->cust_pkg;
-  if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
-    $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
+  if ( $cust_svc ) {
+    my $cust_pkg = $cust_svc->cust_pkg;
+    if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
+      $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
+    }
+  } else {
+    warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
+         "; can't set Expiration\n"
+      unless $cust_svc;
   }
 
   %check;
@@ -1584,7 +1668,7 @@ my %op2condition = (
                $self->$column - $amount <= 0;
              },
   '+' => sub { my($self, $column, $amount) = @_;
-               $self->$column + $amount > 0;
+               ($self->$column || 0) + $amount > 0;
              },
 );
 my %op2warncondition = (
@@ -1593,7 +1677,7 @@ my %op2warncondition = (
                $self->$column - $amount <= $self->$threshold + 0;
              },
   '+' => sub { my($self, $column, $amount) = @_;
-               $self->$column + $amount > 0;
+               ($self->$column || 0) + $amount > 0;
              },
 );
 
@@ -1631,6 +1715,38 @@ sub _op_usage {
   die "Can't update $column for svcnum". $self->svcnum
     if $rv == 0;
 
+  #$self->snapshot; #not necessary, we retain the old values
+  #create an object with the updated usage values
+  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+  #call exports
+  my $error = $new->replace($self);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Error replacing: $error";
+  }
+
+  #overlimit_action eq 'cancel' handling
+  my $cust_pkg = $self->cust_svc->cust_pkg;
+  if ( $cust_pkg
+       && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel' 
+       && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
+     )
+  {
+
+    my $error = $cust_pkg->cancel; #XXX should have a reason
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error cancelling: $error";
+    }
+
+    #nothing else is relevant if we're cancelling, so commit & return success
+    warn "$me update successful; committing\n"
+      if $DEBUG;
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+
+  }
+
   my $action = $op2action{$op};
 
   if ( &{$op2condition{$op}}($self, $column, $amount) &&
@@ -1702,7 +1818,7 @@ sub _op_usage {
 }
 
 sub set_usage {
-  my( $self, $valueref ) = @_;
+  my( $self, $valueref, %options ) = @_;
 
   warn "$me set_usage called for svcnum ". $self->svcnum.
        ' ('. $self->email. "): ".
@@ -1723,6 +1839,11 @@ sub set_usage {
 
   my $reset = 0;
   my %handyhash = ();
+  if ( $options{null} ) { 
+    %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
+                   qw( seconds upbytes downbytes totalbytes )
+                 );
+  }
   foreach my $field (keys %$valueref){
     $reset = 1 if $valueref->{$field};
     $self->setfield($field, $valueref->{$field});
@@ -1741,8 +1862,8 @@ sub set_usage {
   #die $error if $error;         #services not explicity changed via the UI
 
   my $sql = "UPDATE svc_acct SET " .
-    join (',', map { "$_ =  ?" } (keys %handyhash) ).
-    " WHERE svcnum = ?";
+    join (',', map { "$_ =  $handyhash{$_}" } (keys %handyhash) ).
+    " WHERE svcnum = ". $self->svcnum;
 
   warn "$me $sql\n"
     if $DEBUG;
@@ -1750,13 +1871,23 @@ sub set_usage {
   if (scalar(keys %handyhash)) {
     my $sth = $dbh->prepare( $sql )
       or die "Error preparing $sql: ". $dbh->errstr;
-    my $rv = $sth->execute((values %handyhash), $self->svcnum);
+    my $rv = $sth->execute();
     die "Error executing $sql: ". $sth->errstr
       unless defined($rv);
     die "Can't update usage for svcnum ". $self->svcnum
       if $rv == 0;
   }
 
+  #$self->snapshot; #not necessary, we retain the old values
+  #create an object with the updated usage values
+  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+  #call exports
+  my $error = $new->replace($self);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Error replacing: $error";
+  }
+
   if ( $reset ) {
     my $error;
 
@@ -2122,8 +2253,9 @@ sub ldap_password {
   } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
     return '{MD5}'. $1;
   } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
-    die "Blowfish encryption not supported in this context, svcnum ".
-        $self->svcnum. "\n";
+    warn "Blowfish encryption not supported in this context, svcnum ".
+         $self->svcnum. "\n";
+    return '{CRYPT}*'; #unsupported, should not auth
   } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
     return '{SSHA}'. $1;
   } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
@@ -2425,6 +2557,8 @@ 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)
 
+_op_usage and set_usage bypass the history... maybe they shouldn't
+
 =head1 SEE ALSO
 
 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,