4 use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
5 $dir_prefix @shells $usernamemin
6 $usernamemax $passwordmin $passwordmax
7 $username_ampersand $username_letter $username_letterfirst
8 $username_noperiod $username_nounderscore $username_nodash
9 $username_uppercase $username_percent $username_colon
10 $password_noampersand $password_noexclamation
11 $warning_template $warning_from $warning_subject $warning_mimetype
14 $radius_password $radius_ip
17 use Scalar::Util qw( blessed );
22 use Crypt::PasswdMD5 1.2;
23 use Digest::SHA1 'sha1_base64';
24 use Digest::MD5 'md5_base64';
27 use Authen::Passphrase;
28 use FS::UID qw( datasrc driver_name );
30 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
31 use FS::Msgcat qw(gettext);
32 use FS::UI::bytecount;
39 use FS::cust_main_invoice;
44 use FS::radius_usergroup;
51 @ISA = qw( FS::svc_Common );
54 $me = '[FS::svc_acct]';
56 #ask FS::UID to run this stuff for us later
57 FS::UID->install_callback( sub {
59 $dir_prefix = $conf->config('home');
60 @shells = $conf->config('shells');
61 $usernamemin = $conf->config('usernamemin') || 2;
62 $usernamemax = $conf->config('usernamemax');
63 $passwordmin = $conf->config('passwordmin'); # || 6;
65 $passwordmin = ( defined($passwordmin) && $passwordmin =~ /\d+/ )
68 $passwordmax = $conf->config('passwordmax') || 8;
69 $username_letter = $conf->exists('username-letter');
70 $username_letterfirst = $conf->exists('username-letterfirst');
71 $username_noperiod = $conf->exists('username-noperiod');
72 $username_nounderscore = $conf->exists('username-nounderscore');
73 $username_nodash = $conf->exists('username-nodash');
74 $username_uppercase = $conf->exists('username-uppercase');
75 $username_ampersand = $conf->exists('username-ampersand');
76 $username_percent = $conf->exists('username-percent');
77 $username_colon = $conf->exists('username-colon');
78 $password_noampersand = $conf->exists('password-noexclamation');
79 $password_noexclamation = $conf->exists('password-noexclamation');
80 $dirhash = $conf->config('dirhash') || 0;
81 if ( $conf->exists('warning_email') ) {
82 $warning_template = new Text::Template (
84 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
85 ) or warn "can't create warning email template: $Text::Template::ERROR";
86 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
87 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
88 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
89 $warning_cc = $conf->config('warning_email-cc');
91 $warning_template = '';
93 $warning_subject = '';
94 $warning_mimetype = '';
97 $smtpmachine = $conf->config('smtpmachine');
98 $radius_password = $conf->config('radius-password') || 'Password';
99 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
100 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
104 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
105 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
109 my ( $hashref, $cache ) = @_;
110 if ( $hashref->{'svc_acct_svcnum'} ) {
111 $self->{'_domsvc'} = FS::svc_domain->new( {
112 'svcnum' => $hashref->{'domsvc'},
113 'domain' => $hashref->{'svc_acct_domain'},
114 'catchall' => $hashref->{'svc_acct_catchall'},
121 FS::svc_acct - Object methods for svc_acct records
127 $record = new FS::svc_acct \%hash;
128 $record = new FS::svc_acct { 'column' => 'value' };
130 $error = $record->insert;
132 $error = $new_record->replace($old_record);
134 $error = $record->delete;
136 $error = $record->check;
138 $error = $record->suspend;
140 $error = $record->unsuspend;
142 $error = $record->cancel;
144 %hash = $record->radius;
146 %hash = $record->radius_reply;
148 %hash = $record->radius_check;
150 $domain = $record->domain;
152 $svc_domain = $record->svc_domain;
154 $email = $record->email;
156 $seconds_since = $record->seconds_since($timestamp);
160 An FS::svc_acct object represents an account. FS::svc_acct inherits from
161 FS::svc_Common. The following fields are currently supported:
167 Primary key (assigned automatcially for new accounts)
175 =item _password_encoding
177 plain, crypt, ldap (or empty for autodetection)
185 Point of presence (see L<FS::svc_acct_pop>)
197 set automatically if blank (and uid is not)
217 svcnum from svc_domain
221 Optional svcnum from svc_pbx
223 =item radius_I<Radius_Attribute>
225 I<Radius-Attribute> (reply)
227 =item rc_I<Radius_Attribute>
229 I<Radius-Attribute> (check)
239 Creates a new account. To add the account to the database, see L<"insert">.
246 'longname_plural' => 'Access accounts and mailboxes',
247 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
248 'display_weight' => 10,
249 'cancel_weight' => 50,
251 'dir' => 'Home directory',
254 def_info => 'set to fixed and blank for no UIDs',
257 'slipip' => 'IP address',
258 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
260 label => 'Access number',
262 select_table => 'svc_acct_pop',
263 select_key => 'popnum',
264 select_label => 'city',
270 disable_default => 1,
275 label => 'Communigate account type',
277 select_list => [qw( MultiMailbox TextMailbox MailDirMailbox AGrade BGrade CGrade )],
278 disable_inventory => 1,
281 'cgp_accessmodes' => {
282 label => 'Communigate enabled services',
283 type => 'communigate_pro-accessmodes',
284 disable_inventory => 1,
288 label => 'Communigate aliases',
290 disable_inventory => 1,
293 'cgp_deletemode' => {
294 label => 'Communigate message delete method',
296 select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
297 disable_inventory => 1,
300 'cgp_emptytrash' => {
301 label => 'Communigate on logout remove trash',
303 disable_inventory => 1,
307 label => 'Quota', #Mail storage limit
309 disable_inventory => 1,
313 label => 'File storage limit',
315 disable_inventory => 1,
319 label => 'Number of files limit',
321 disable_inventory => 1,
325 label => 'File size limit',
327 disable_inventory => 1,
330 '_password' => 'Password',
333 def_info => 'when blank, defaults to UID',
338 def_info => 'set to blank for no shell tracking',
340 #select_list => [ $conf->config('shells') ],
341 select_list => [ $conf ? $conf->config('shells') : () ],
342 disable_inventory => 1,
345 'finger' => 'Real name', # (GECOS)',
349 select_table => 'svc_domain',
350 select_key => 'svcnum',
351 select_label => 'domain',
352 disable_inventory => 1,
358 select_table => 'svc_domain',
359 select_key => 'svcnum',
360 select_label => 'domain',
361 disable_inventory => 1,
364 'pbxsvc' => { label => 'PBX',
365 type => 'select-svc_pbx.html',
366 disable_inventory => 1,
367 disable_select => 1, #UI wonky, pry works otherwise
370 label => 'RADIUS groups',
371 type => 'radius_usergroup_selector',
372 disable_inventory => 1,
375 'seconds' => { label => 'Seconds',
376 label_sort => 'with Time Remaining',
378 disable_inventory => 1,
380 disable_part_svc_column => 1,
382 'upbytes' => { label => 'Upload',
384 disable_inventory => 1,
386 'format' => \&FS::UI::bytecount::display_bytecount,
387 'parse' => \&FS::UI::bytecount::parse_bytecount,
388 disable_part_svc_column => 1,
390 'downbytes' => { label => 'Download',
392 disable_inventory => 1,
394 'format' => \&FS::UI::bytecount::display_bytecount,
395 'parse' => \&FS::UI::bytecount::parse_bytecount,
396 disable_part_svc_column => 1,
398 'totalbytes'=> { label => 'Total up and download',
400 disable_inventory => 1,
402 'format' => \&FS::UI::bytecount::display_bytecount,
403 'parse' => \&FS::UI::bytecount::parse_bytecount,
404 disable_part_svc_column => 1,
406 'seconds_threshold' => { label => 'Seconds threshold',
408 disable_inventory => 1,
410 disable_part_svc_column => 1,
412 'upbytes_threshold' => { label => 'Upload threshold',
414 disable_inventory => 1,
416 'format' => \&FS::UI::bytecount::display_bytecount,
417 'parse' => \&FS::UI::bytecount::parse_bytecount,
418 disable_part_svc_column => 1,
420 'downbytes_threshold' => { label => 'Download threshold',
422 disable_inventory => 1,
424 'format' => \&FS::UI::bytecount::display_bytecount,
425 'parse' => \&FS::UI::bytecount::parse_bytecount,
426 disable_part_svc_column => 1,
428 'totalbytes_threshold'=> { label => 'Total up and download threshold',
430 disable_inventory => 1,
432 'format' => \&FS::UI::bytecount::display_bytecount,
433 'parse' => \&FS::UI::bytecount::parse_bytecount,
434 disable_part_svc_column => 1,
437 label => 'Last login',
441 label => 'Last logout',
448 sub table { 'svc_acct'; }
450 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
454 #false laziness with edit/svc_acct.cgi
456 my( $self, $groups ) = @_;
457 if ( ref($groups) eq 'ARRAY' ) {
459 } elsif ( length($groups) ) {
460 [ split(/\s*,\s*/, $groups) ];
469 shift->_lastlog('in', @_);
473 shift->_lastlog('out', @_);
477 my( $self, $op, $time ) = @_;
479 if ( defined($time) ) {
480 warn "$me last_log$op called on svcnum ". $self->svcnum.
481 ' ('. $self->email. "): $time\n"
486 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
490 my $sth = $dbh->prepare( $sql )
491 or die "Error preparing $sql: ". $dbh->errstr;
492 my $rv = $sth->execute($time, $self->svcnum);
493 die "Error executing $sql: ". $sth->errstr
495 die "Can't update last_log$op for svcnum". $self->svcnum
498 $self->{'Hash'}->{"last_log$op"} = $time;
500 $self->getfield("last_log$op");
504 =item search_sql STRING
506 Class method which returns an SQL fragment to search for the given string.
511 my( $class, $string ) = @_;
512 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
513 my( $username, $domain ) = ( $1, $2 );
514 my $q_username = dbh->quote($username);
515 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
517 "svc_acct.username = $q_username AND ( ".
518 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
523 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
525 $class->search_sql_field('slipip', $string ).
527 $class->search_sql_field('username', $string ).
530 $class->search_sql_field('username', $string);
534 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
536 Returns the "username@domain" string for this account.
538 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
548 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
550 Returns a longer string label for this acccount ("Real Name <username@domain>"
551 if available, or "username@domain").
553 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
560 my $label = $self->label(@_);
561 my $finger = $self->finger;
562 return $label unless $finger =~ /\S/;
563 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
564 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
568 =item insert [ , OPTION => VALUE ... ]
570 Adds this account to the database. If there is an error, returns the error,
571 otherwise returns false.
573 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
574 defined. An FS::cust_svc record will be created and inserted.
576 The additional field I<usergroup> can optionally be defined; if so it should
577 contain an arrayref of group names. See L<FS::radius_usergroup>.
579 The additional field I<child_objects> can optionally be defined; if so it
580 should contain an arrayref of FS::tablename objects. They will have their
581 svcnum fields set and will be inserted after this record, but before any
582 exports are run. Each element of the array can also optionally be a
583 two-element array reference containing the child object and the name of an
584 alternate field to be filled in with the newly-inserted svcnum, for example
585 C<[ $svc_forward, 'srcsvc' ]>
587 Currently available options are: I<depend_jobnum>
589 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
590 jobnums), all provisioning jobs will have a dependancy on the supplied
591 jobnum(s) (they will not run until the specific job(s) complete(s)).
593 (TODOC: L<FS::queue> and L<freeside-queued>)
595 (TODOC: new exports!)
604 warn "[$me] insert called on $self: ". Dumper($self).
605 "\nwith options: ". Dumper(%options);
608 local $SIG{HUP} = 'IGNORE';
609 local $SIG{INT} = 'IGNORE';
610 local $SIG{QUIT} = 'IGNORE';
611 local $SIG{TERM} = 'IGNORE';
612 local $SIG{TSTP} = 'IGNORE';
613 local $SIG{PIPE} = 'IGNORE';
615 my $oldAutoCommit = $FS::UID::AutoCommit;
616 local $FS::UID::AutoCommit = 0;
620 my $error = $self->SUPER::insert(
621 'jobnums' => \@jobnums,
622 'child_objects' => $self->child_objects,
626 $dbh->rollback if $oldAutoCommit;
630 if ( $self->usergroup ) {
631 foreach my $groupname ( @{$self->usergroup} ) {
632 my $radius_usergroup = new FS::radius_usergroup ( {
633 svcnum => $self->svcnum,
634 groupname => $groupname,
636 my $error = $radius_usergroup->insert;
638 $dbh->rollback if $oldAutoCommit;
644 unless ( $skip_fuzzyfiles ) {
645 $error = $self->queue_fuzzyfiles_update;
647 $dbh->rollback if $oldAutoCommit;
648 return "updating fuzzy search cache: $error";
652 my $cust_pkg = $self->cust_svc->cust_pkg;
655 my $cust_main = $cust_pkg->cust_main;
656 my $agentnum = $cust_main->agentnum;
658 if ( $conf->exists('emailinvoiceautoalways')
659 || $conf->exists('emailinvoiceauto')
660 && ! $cust_main->invoicing_list_emailonly
662 my @invoicing_list = $cust_main->invoicing_list;
663 push @invoicing_list, $self->email;
664 $cust_main->invoicing_list(\@invoicing_list);
668 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
669 = ('','','','','','');
671 if ( $conf->exists('welcome_email', $agentnum) ) {
672 $welcome_template = new Text::Template (
674 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
675 ) or warn "can't create welcome email template: $Text::Template::ERROR";
676 $welcome_from = $conf->config('welcome_email-from', $agentnum);
677 # || 'your-isp-is-dum'
678 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
680 $welcome_subject_template = new Text::Template (
682 SOURCE => $welcome_subject,
683 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
684 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
687 if ( $welcome_template && $cust_pkg ) {
688 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
692 'custnum' => $self->custnum,
693 'username' => $self->username,
694 'password' => $self->_password,
695 'first' => $cust_main->first,
696 'last' => $cust_main->getfield('last'),
697 'pkg' => $cust_pkg->part_pkg->pkg,
699 my $wqueue = new FS::queue {
700 'svcnum' => $self->svcnum,
701 'job' => 'FS::svc_acct::send_email'
703 my $error = $wqueue->insert(
705 'from' => $welcome_from,
706 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
707 'mimetype' => $welcome_mimetype,
708 'body' => $welcome_template->fill_in( HASH => \%hash, ),
711 $dbh->rollback if $oldAutoCommit;
712 return "error queuing welcome email: $error";
715 if ( $options{'depend_jobnum'} ) {
716 warn "$me depend_jobnum found; adding to welcome email dependancies"
718 if ( ref($options{'depend_jobnum'}) ) {
719 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
720 "to welcome email dependancies"
722 push @jobnums, @{ $options{'depend_jobnum'} };
724 warn "$me adding job $options{'depend_jobnum'} ".
725 "to welcome email dependancies"
727 push @jobnums, $options{'depend_jobnum'};
731 foreach my $jobnum ( @jobnums ) {
732 my $error = $wqueue->depend_insert($jobnum);
734 $dbh->rollback if $oldAutoCommit;
735 return "error queuing welcome email job dependancy: $error";
745 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
749 # set usage fields and thresholds if unset but set in a package def
750 # AND the package already has a last bill date (otherwise they get double added)
751 sub preinsert_hook_first {
754 return '' unless $self->pkgnum;
756 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
757 return '' unless $cust_pkg && $cust_pkg->last_bill;
759 my $part_pkg = $cust_pkg->part_pkg;
760 return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
762 my %values = $part_pkg->usage_valuehash;
763 my $multiplier = $conf->exists('svc_acct-usage_threshold')
764 ? 1 - $conf->config('svc_acct-usage_threshold')/100
765 : 0.20; #doesn't matter
767 foreach ( keys %values ) {
768 next if $self->getfield($_);
769 $self->setfield( $_, $values{$_} );
770 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
771 if $conf->exists('svc_acct-usage_threshold');
779 Deletes this account from the database. If there is an error, returns the
780 error, otherwise returns false.
782 The corresponding FS::cust_svc record will be deleted as well.
784 (TODOC: new exports!)
791 return "can't delete system account" if $self->_check_system;
793 return "Can't delete an account which is a (svc_forward) source!"
794 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
796 return "Can't delete an account which is a (svc_forward) destination!"
797 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
799 return "Can't delete an account with (svc_www) web service!"
800 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
802 # what about records in session ? (they should refer to history table)
804 local $SIG{HUP} = 'IGNORE';
805 local $SIG{INT} = 'IGNORE';
806 local $SIG{QUIT} = 'IGNORE';
807 local $SIG{TERM} = 'IGNORE';
808 local $SIG{TSTP} = 'IGNORE';
809 local $SIG{PIPE} = 'IGNORE';
811 my $oldAutoCommit = $FS::UID::AutoCommit;
812 local $FS::UID::AutoCommit = 0;
815 foreach my $cust_main_invoice (
816 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
818 unless ( defined($cust_main_invoice) ) {
819 warn "WARNING: something's wrong with qsearch";
822 my %hash = $cust_main_invoice->hash;
823 $hash{'dest'} = $self->email;
824 my $new = new FS::cust_main_invoice \%hash;
825 my $error = $new->replace($cust_main_invoice);
827 $dbh->rollback if $oldAutoCommit;
832 foreach my $svc_domain (
833 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
835 my %hash = new FS::svc_domain->hash;
836 $hash{'catchall'} = '';
837 my $new = new FS::svc_domain \%hash;
838 my $error = $new->replace($svc_domain);
840 $dbh->rollback if $oldAutoCommit;
845 my $error = $self->SUPER::delete;
847 $dbh->rollback if $oldAutoCommit;
851 foreach my $radius_usergroup (
852 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
854 my $error = $radius_usergroup->delete;
856 $dbh->rollback if $oldAutoCommit;
861 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
865 =item replace OLD_RECORD
867 Replaces OLD_RECORD with this one in the database. If there is an error,
868 returns the error, otherwise returns false.
870 The additional field I<usergroup> can optionally be defined; if so it should
871 contain an arrayref of group names. See L<FS::radius_usergroup>.
879 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
883 warn "$me replacing $old with $new\n" if $DEBUG;
887 return "can't modify system account" if $old->_check_system;
890 #no warnings 'numeric'; #alas, a 5.006-ism
893 foreach my $xid (qw( uid gid )) {
895 return "Can't change $xid!"
896 if ! $conf->exists("svc_acct-edit_$xid")
897 && $old->$xid() != $new->$xid()
898 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
903 #change homdir when we change username
904 $new->setfield('dir', '') if $old->username ne $new->username;
906 local $SIG{HUP} = 'IGNORE';
907 local $SIG{INT} = 'IGNORE';
908 local $SIG{QUIT} = 'IGNORE';
909 local $SIG{TERM} = 'IGNORE';
910 local $SIG{TSTP} = 'IGNORE';
911 local $SIG{PIPE} = 'IGNORE';
913 my $oldAutoCommit = $FS::UID::AutoCommit;
914 local $FS::UID::AutoCommit = 0;
917 # redundant, but so $new->usergroup gets set
918 $error = $new->check;
919 return $error if $error;
921 $old->usergroup( [ $old->radius_groups ] );
923 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
924 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
926 if ( $new->usergroup ) {
927 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
928 my @newgroups = @{$new->usergroup};
929 foreach my $oldgroup ( @{$old->usergroup} ) {
930 if ( grep { $oldgroup eq $_ } @newgroups ) {
931 @newgroups = grep { $oldgroup ne $_ } @newgroups;
934 my $radius_usergroup = qsearchs('radius_usergroup', {
935 svcnum => $old->svcnum,
936 groupname => $oldgroup,
938 my $error = $radius_usergroup->delete;
940 $dbh->rollback if $oldAutoCommit;
941 return "error deleting radius_usergroup $oldgroup: $error";
945 foreach my $newgroup ( @newgroups ) {
946 my $radius_usergroup = new FS::radius_usergroup ( {
947 svcnum => $new->svcnum,
948 groupname => $newgroup,
950 my $error = $radius_usergroup->insert;
952 $dbh->rollback if $oldAutoCommit;
953 return "error adding radius_usergroup $newgroup: $error";
959 $error = $new->SUPER::replace($old, @_);
961 $dbh->rollback if $oldAutoCommit;
962 return $error if $error;
965 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
966 $error = $new->queue_fuzzyfiles_update;
968 $dbh->rollback if $oldAutoCommit;
969 return "updating fuzzy search cache: $error";
973 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
977 =item queue_fuzzyfiles_update
979 Used by insert & replace to update the fuzzy search cache
983 sub queue_fuzzyfiles_update {
986 local $SIG{HUP} = 'IGNORE';
987 local $SIG{INT} = 'IGNORE';
988 local $SIG{QUIT} = 'IGNORE';
989 local $SIG{TERM} = 'IGNORE';
990 local $SIG{TSTP} = 'IGNORE';
991 local $SIG{PIPE} = 'IGNORE';
993 my $oldAutoCommit = $FS::UID::AutoCommit;
994 local $FS::UID::AutoCommit = 0;
997 my $queue = new FS::queue {
998 'svcnum' => $self->svcnum,
999 'job' => 'FS::svc_acct::append_fuzzyfiles'
1001 my $error = $queue->insert($self->username);
1003 $dbh->rollback if $oldAutoCommit;
1004 return "queueing job (transaction rolled back): $error";
1007 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1015 Suspends this account by calling export-specific suspend hooks. If there is
1016 an error, returns the error, otherwise returns false.
1018 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1024 return "can't suspend system account" if $self->_check_system;
1025 $self->SUPER::suspend(@_);
1030 Unsuspends this account by by calling export-specific suspend hooks. If there
1031 is an error, returns the error, otherwise returns false.
1033 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1039 my %hash = $self->hash;
1040 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
1041 $hash{_password} = $1;
1042 my $new = new FS::svc_acct ( \%hash );
1043 my $error = $new->replace($self);
1044 return $error if $error;
1047 $self->SUPER::unsuspend(@_);
1052 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1054 If the B<auto_unset_catchall> configuration option is set, this method will
1055 automatically remove any references to the canceled service in the catchall
1056 field of svc_domain. This allows packages that contain both a svc_domain and
1057 its catchall svc_acct to be canceled in one step.
1062 # Only one thing to do at this level
1064 foreach my $svc_domain (
1065 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
1066 if($conf->exists('auto_unset_catchall')) {
1067 my %hash = $svc_domain->hash;
1068 $hash{catchall} = '';
1069 my $new = new FS::svc_domain ( \%hash );
1070 my $error = $new->replace($svc_domain);
1071 return $error if $error;
1073 return "cannot unprovision svc_acct #".$self->svcnum.
1074 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
1078 $self->SUPER::cancel(@_);
1084 Checks all fields to make sure this is a valid service. If there is an error,
1085 returns the error, otherwise returns false. Called by the insert and replace
1088 Sets any fixed values; see L<FS::part_svc>.
1095 my($recref) = $self->hashref;
1097 my $x = $self->setfixed( $self->_fieldhandlers );
1098 return $x unless ref($x);
1101 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1103 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1106 my $error = $self->ut_numbern('svcnum')
1107 #|| $self->ut_number('domsvc')
1108 || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
1109 || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
1110 || $self->ut_textn('sec_phrase')
1111 || $self->ut_snumbern('seconds')
1112 || $self->ut_snumbern('upbytes')
1113 || $self->ut_snumbern('downbytes')
1114 || $self->ut_snumbern('totalbytes')
1115 || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
1116 || $self->ut_enum('password_selfchange', [ '', 'Y' ])
1117 || $self->ut_enum('password_recover', [ '', 'Y' ])
1118 || $self->ut_textn('cgp_accessmodes')
1119 || $self->ut_alphan('cgp_type')
1120 || $self->ut_textn('cgp_aliases' ) #well
1121 || $self->ut_alphasn('cgp_deletemode')
1122 || $self->ut_alphan('cgp_emptytrash')
1124 return $error if $error;
1127 local $username_letter = $username_letter;
1128 if ($self->svcnum) {
1129 my $cust_svc = $self->cust_svc
1130 or return "no cust_svc record found for svcnum ". $self->svcnum;
1131 my $cust_pkg = $cust_svc->cust_pkg;
1133 if ($self->pkgnum) {
1134 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );#complain?
1138 $conf->exists('username-letter', $cust_pkg->cust_main->agentnum);
1141 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1142 if ( $username_uppercase ) {
1143 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1144 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1145 $recref->{username} = $1;
1147 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1148 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1149 $recref->{username} = $1;
1152 if ( $username_letterfirst ) {
1153 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1154 } elsif ( $username_letter ) {
1155 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1157 if ( $username_noperiod ) {
1158 $recref->{username} =~ /\./ and return gettext('illegal_username');
1160 if ( $username_nounderscore ) {
1161 $recref->{username} =~ /_/ and return gettext('illegal_username');
1163 if ( $username_nodash ) {
1164 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1166 unless ( $username_ampersand ) {
1167 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1169 unless ( $username_percent ) {
1170 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1172 unless ( $username_colon ) {
1173 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1176 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1177 $recref->{popnum} = $1;
1178 return "Unknown popnum" unless
1179 ! $recref->{popnum} ||
1180 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1182 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1184 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1185 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1187 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1188 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1189 #not all systems use gid=uid
1190 #you can set a fixed gid in part_svc
1192 return "Only root can have uid 0"
1193 if $recref->{uid} == 0
1194 && $recref->{username} !~ /^(root|toor|smtp)$/;
1196 unless ( $recref->{username} eq 'sync' ) {
1197 if ( grep $_ eq $recref->{shell}, @shells ) {
1198 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1200 return "Illegal shell \`". $self->shell. "\'; ".
1201 "shells configuration value contains: @shells";
1204 $recref->{shell} = '/bin/sync';
1208 $recref->{gid} ne '' ?
1209 return "Can't have gid without uid" : ( $recref->{gid}='' );
1210 #$recref->{dir} ne '' ?
1211 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1212 $recref->{shell} ne '' ?
1213 return "Can't have shell without uid" : ( $recref->{shell}='' );
1216 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1218 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1219 or return "Illegal directory: ". $recref->{dir};
1220 $recref->{dir} = $1;
1221 return "Illegal directory"
1222 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1223 return "Illegal directory"
1224 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1225 unless ( $recref->{dir} ) {
1226 $recref->{dir} = $dir_prefix . '/';
1227 if ( $dirhash > 0 ) {
1228 for my $h ( 1 .. $dirhash ) {
1229 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1231 } elsif ( $dirhash < 0 ) {
1232 for my $h ( reverse $dirhash .. -1 ) {
1233 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1236 $recref->{dir} .= $recref->{username};
1242 # $error = $self->ut_textn('finger');
1243 # return $error if $error;
1244 if ( $self->getfield('finger') eq '' ) {
1245 my $cust_pkg = $self->svcnum
1246 ? $self->cust_svc->cust_pkg
1247 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1249 my $cust_main = $cust_pkg->cust_main;
1250 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1253 $self->getfield('finger') =~
1254 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1255 or return "Illegal finger: ". $self->getfield('finger');
1256 $self->setfield('finger', $1);
1258 for (qw( quota file_quota file_maxsize )) {
1259 $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
1262 $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
1263 $recref->{file_maxnum} = $1;
1265 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1266 if ( $recref->{slipip} eq '' ) {
1267 $recref->{slipip} = '';
1268 } elsif ( $recref->{slipip} eq '0e0' ) {
1269 $recref->{slipip} = '0e0';
1271 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1272 or return "Illegal slipip: ". $self->slipip;
1273 $recref->{slipip} = $1;
1278 #arbitrary RADIUS stuff; allow ut_textn for now
1279 foreach ( grep /^radius_/, fields('svc_acct') ) {
1280 $self->ut_textn($_);
1283 # First, if _password is blank, generate one and set default encoding.
1284 if ( ! $recref->{_password} ) {
1285 $error = $self->set_password('');
1287 # But if there's a _password but no encoding, assume it's plaintext and
1288 # set it to default encoding.
1289 elsif ( ! $recref->{_password_encoding} ) {
1290 $error = $self->set_password($recref->{_password});
1292 return $error if $error;
1294 # Next, check _password to ensure compliance with the encoding.
1295 if ( $recref->{_password_encoding} eq 'ldap' ) {
1297 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1298 $recref->{_password} = uc($1).$2;
1300 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1303 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1305 if ( $recref->{_password} =~
1306 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1307 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1310 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1313 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1316 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1317 # Password randomization is now in set_password.
1318 # Strip whitespace characters, check length requirements, etc.
1319 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1320 $recref->{_password} = $1;
1322 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1323 FS::Msgcat::_gettext('illegal_password_characters').
1324 ": ". $recref->{_password};
1327 if ( $password_noampersand ) {
1328 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1330 if ( $password_noexclamation ) {
1331 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1335 return "invalid password encoding ('".$recref->{_password_encoding}."'";
1337 $self->SUPER::check;
1342 sub _password_encryption {
1344 my $encoding = lc($self->_password_encoding);
1345 return if !$encoding;
1346 return 'plain' if $encoding eq 'plain';
1347 if($encoding eq 'crypt') {
1348 my $pass = $self->_password;
1349 $pass =~ s/^\*SUSPENDED\* //;
1351 return 'md5' if $pass =~ /^\$1\$/;
1352 #return 'blowfish' if $self->_password =~ /^\$2\$/;
1353 return 'des' if length($pass) == 13;
1356 if($encoding eq 'ldap') {
1357 uc($self->_password) =~ /^\{([\w-]+)\}/;
1358 return 'crypt' if $1 eq 'CRYPT' or $1 eq 'DES';
1359 return 'plain' if $1 eq 'PLAIN' or $1 eq 'CLEARTEXT';
1360 return 'md5' if $1 eq 'MD5';
1361 return 'sha1' if $1 eq 'SHA' or $1 eq 'SHA-1';
1368 sub get_cleartext_password {
1370 if($self->_password_encryption eq 'plain') {
1371 if($self->_password_encoding eq 'ldap') {
1372 $self->_password =~ /\{\w+\}(.*)$/;
1376 return $self->_password;
1385 Set the cleartext password for the account. If _password_encoding is set, the
1386 new password will be encoded according to the existing method (including
1387 encryption mode, if it can be determined). Otherwise,
1388 config('default-password-encoding') is used.
1390 If no password is supplied (or a zero-length password when minimum password length
1391 is >0), one will be generated randomly.
1396 my( $self, $pass ) = ( shift, shift );
1398 warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
1401 my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
1402 FS::Msgcat::_gettext('illegal_password_characters').
1405 my( $encoding, $encryption ) = ('', '');
1407 if ( $self->_password_encoding ) {
1408 $encoding = $self->_password_encoding;
1409 # identify existing encryption method, try to use it.
1410 $encryption = $self->_password_encryption;
1412 # use the system default
1418 # set encoding to system default
1419 ($encoding, $encryption) =
1420 split(/-/, lc($conf->config('default-password-encoding')));
1421 $encoding ||= 'legacy';
1422 $self->_password_encoding($encoding);
1425 if ( $encoding eq 'legacy' ) {
1427 # The legacy behavior from check():
1428 # If the password is blank, randomize it and set encoding to 'plain'.
1429 if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
1430 $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1431 $self->_password_encoding('plain');
1433 # Prefix + valid-length password
1434 if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1436 $self->_password_encoding('plain');
1437 # Prefix + crypt string
1438 } elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1440 $self->_password_encoding('crypt');
1441 # Various disabled crypt passwords
1442 } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
1443 $self->_password_encoding('crypt');
1449 $self->_password($pass);
1455 if $passwordmin && length($pass) < $passwordmin
1456 or $passwordmax && length($pass) > $passwordmax;
1458 if ( $encoding eq 'crypt' ) {
1459 if ($encryption eq 'md5') {
1460 $pass = unix_md5_crypt($pass);
1461 } elsif ($encryption eq 'des') {
1462 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1465 } elsif ( $encoding eq 'ldap' ) {
1466 if ($encryption eq 'md5') {
1467 $pass = md5_base64($pass);
1468 } elsif ($encryption eq 'sha1') {
1469 $pass = sha1_base64($pass);
1470 } elsif ($encryption eq 'crypt') {
1471 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1473 # else $encryption eq 'plain', do nothing
1474 $pass = '{'.uc($encryption).'}'.$pass;
1476 # else encoding eq 'plain'
1478 $self->_password($pass);
1484 Internal function to check the username against the list of system usernames
1485 from the I<system_usernames> configuration value. Returns true if the username
1486 is listed on the system username list.
1492 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1493 $conf->config('system_usernames')
1497 =item _check_duplicate
1499 Internal method to check for duplicates usernames, username@domain pairs and
1502 If the I<global_unique-username> configuration value is set to B<username> or
1503 B<username@domain>, enforces global username or username@domain uniqueness.
1505 In all cases, check for duplicate uids and usernames or username@domain pairs
1506 per export and with identical I<svcpart> values.
1510 sub _check_duplicate {
1513 my $global_unique = $conf->config('global_unique-username') || 'none';
1514 return '' if $global_unique eq 'disabled';
1518 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1519 unless ( $part_svc ) {
1520 return 'unknown svcpart '. $self->svcpart;
1523 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1524 qsearch( 'svc_acct', { 'username' => $self->username } );
1525 return gettext('username_in_use')
1526 if $global_unique eq 'username' && @dup_user;
1528 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1529 qsearch( 'svc_acct', { 'username' => $self->username,
1530 'domsvc' => $self->domsvc } );
1531 return gettext('username_in_use')
1532 if $global_unique eq 'username@domain' && @dup_userdomain;
1535 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1536 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1537 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1538 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1543 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1544 my $exports = FS::part_export::export_info('svc_acct');
1545 my %conflict_user_svcpart;
1546 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1548 foreach my $part_export ( $part_svc->part_export ) {
1550 #this will catch to the same exact export
1551 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1553 #this will catch to exports w/same exporthost+type ???
1554 #my @other_part_export = qsearch('part_export', {
1555 # 'machine' => $part_export->machine,
1556 # 'exporttype' => $part_export->exporttype,
1558 #foreach my $other_part_export ( @other_part_export ) {
1559 # push @svcparts, map { $_->svcpart }
1560 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1563 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1564 #silly kludge to avoid uninitialized value errors
1565 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1566 ? $exports->{$part_export->exporttype}{'nodomain'}
1568 if ( $nodomain =~ /^Y/i ) {
1569 $conflict_user_svcpart{$_} = $part_export->exportnum
1572 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1577 foreach my $dup_user ( @dup_user ) {
1578 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1579 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1580 return "duplicate username ". $self->username.
1581 ": conflicts with svcnum ". $dup_user->svcnum.
1582 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1586 foreach my $dup_userdomain ( @dup_userdomain ) {
1587 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1588 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1589 return "duplicate username\@domain ". $self->email.
1590 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1591 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1595 foreach my $dup_uid ( @dup_uid ) {
1596 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1597 if ( exists($conflict_user_svcpart{$dup_svcpart})
1598 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1599 return "duplicate uid ". $self->uid.
1600 ": conflicts with svcnum ". $dup_uid->svcnum.
1602 ( $conflict_user_svcpart{$dup_svcpart}
1603 || $conflict_userdomain_svcpart{$dup_svcpart} );
1615 Depriciated, use radius_reply instead.
1620 carp "FS::svc_acct::radius depriciated, use radius_reply";
1621 $_[0]->radius_reply;
1626 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1627 reply attributes of this record.
1629 Note that this is now the preferred method for reading RADIUS attributes -
1630 accessing the columns directly is discouraged, as the column names are
1631 expected to change in the future.
1638 return %{ $self->{'radius_reply'} }
1639 if exists $self->{'radius_reply'};
1644 my($column, $attrib) = ($1, $2);
1645 #$attrib =~ s/_/\-/g;
1646 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1647 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1649 if ( $self->slipip && $self->slipip ne '0e0' ) {
1650 $reply{$radius_ip} = $self->slipip;
1653 if ( $self->seconds !~ /^$/ ) {
1654 $reply{'Session-Timeout'} = $self->seconds;
1657 if ( $conf->exists('radius-chillispot-max') ) {
1658 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1660 #hmm. just because sqlradius.pm says so?
1667 foreach my $what (qw( input output total )) {
1668 my $is = $whatis{$what}.'bytes';
1669 if ( $self->$is() =~ /\d/ ) {
1670 my $big = new Math::BigInt $self->$is();
1671 $big = new Math::BigInt '0' if $big->is_neg();
1672 my $att = "Chillispot-Max-\u$what";
1673 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1674 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1685 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1686 check attributes of this record.
1688 Note that this is now the preferred method for reading RADIUS attributes -
1689 accessing the columns directly is discouraged, as the column names are
1690 expected to change in the future.
1697 return %{ $self->{'radius_check'} }
1698 if exists $self->{'radius_check'};
1703 my($column, $attrib) = ($1, $2);
1704 #$attrib =~ s/_/\-/g;
1705 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1706 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1709 my($pw_attrib, $password) = $self->radius_password;
1710 $check{$pw_attrib} = $password;
1712 my $cust_svc = $self->cust_svc;
1714 my $cust_pkg = $cust_svc->cust_pkg;
1715 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1716 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1719 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1720 "; can't set Expiration\n"
1728 =item radius_password
1730 Returns a key/value pair containing the RADIUS attribute name and value
1735 sub radius_password {
1739 if ( $self->_password_encoding eq 'ldap' ) {
1740 $pw_attrib = 'Password-With-Header';
1741 } elsif ( $self->_password_encoding eq 'crypt' ) {
1742 $pw_attrib = 'Crypt-Password';
1743 } elsif ( $self->_password_encoding eq 'plain' ) {
1744 $pw_attrib = $radius_password;
1746 $pw_attrib = length($self->_password) <= 12
1751 ($pw_attrib, $self->_password);
1757 This method instructs the object to "snapshot" or freeze RADIUS check and
1758 reply attributes to the current values.
1762 #bah, my english is too broken this morning
1763 #Of note is the "Expiration" attribute, which, for accounts in prepaid packages, is typically defined on-the-fly as the associated packages cust_pkg.bill. (This is used by
1764 #the FS::cust_pkg's replace method to trigger the correct export updates when
1765 #package dates change)
1770 $self->{$_} = { $self->$_() }
1771 foreach qw( radius_reply radius_check );
1775 =item forget_snapshot
1777 This methos instructs the object to forget any previously snapshotted
1778 RADIUS check and reply attributes.
1782 sub forget_snapshot {
1786 foreach qw( radius_reply radius_check );
1790 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1792 Returns the domain associated with this account.
1794 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1801 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1802 my $svc_domain = $self->svc_domain(@_)
1803 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1804 $svc_domain->domain;
1809 Returns the FS::svc_domain record for this account's domain (see
1814 # FS::h_svc_acct has a history-aware svc_domain override
1819 ? $self->{'_domsvc'}
1820 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1825 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1829 #inherited from svc_Common
1831 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1833 Returns an email address associated with the account.
1835 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1842 $self->username. '@'. $self->domain(@_);
1847 Returns an array of FS::acct_snarf records associated with the account.
1848 If the acct_snarf table does not exist or there are no associated records,
1849 an empty list is returned
1855 return () unless dbdef->table('acct_snarf');
1856 eval "use FS::acct_snarf;";
1858 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1861 =item decrement_upbytes OCTETS
1863 Decrements the I<upbytes> field of this record by the given amount. If there
1864 is an error, returns the error, otherwise returns false.
1868 sub decrement_upbytes {
1869 shift->_op_usage('-', 'upbytes', @_);
1872 =item increment_upbytes OCTETS
1874 Increments the I<upbytes> field of this record by the given amount. If there
1875 is an error, returns the error, otherwise returns false.
1879 sub increment_upbytes {
1880 shift->_op_usage('+', 'upbytes', @_);
1883 =item decrement_downbytes OCTETS
1885 Decrements the I<downbytes> field of this record by the given amount. If there
1886 is an error, returns the error, otherwise returns false.
1890 sub decrement_downbytes {
1891 shift->_op_usage('-', 'downbytes', @_);
1894 =item increment_downbytes OCTETS
1896 Increments the I<downbytes> field of this record by the given amount. If there
1897 is an error, returns the error, otherwise returns false.
1901 sub increment_downbytes {
1902 shift->_op_usage('+', 'downbytes', @_);
1905 =item decrement_totalbytes OCTETS
1907 Decrements the I<totalbytes> field of this record by the given amount. If there
1908 is an error, returns the error, otherwise returns false.
1912 sub decrement_totalbytes {
1913 shift->_op_usage('-', 'totalbytes', @_);
1916 =item increment_totalbytes OCTETS
1918 Increments the I<totalbytes> field of this record by the given amount. If there
1919 is an error, returns the error, otherwise returns false.
1923 sub increment_totalbytes {
1924 shift->_op_usage('+', 'totalbytes', @_);
1927 =item decrement_seconds SECONDS
1929 Decrements the I<seconds> field of this record by the given amount. If there
1930 is an error, returns the error, otherwise returns false.
1934 sub decrement_seconds {
1935 shift->_op_usage('-', 'seconds', @_);
1938 =item increment_seconds SECONDS
1940 Increments the I<seconds> field of this record by the given amount. If there
1941 is an error, returns the error, otherwise returns false.
1945 sub increment_seconds {
1946 shift->_op_usage('+', 'seconds', @_);
1954 my %op2condition = (
1955 '-' => sub { my($self, $column, $amount) = @_;
1956 $self->$column - $amount <= 0;
1958 '+' => sub { my($self, $column, $amount) = @_;
1959 ($self->$column || 0) + $amount > 0;
1962 my %op2warncondition = (
1963 '-' => sub { my($self, $column, $amount) = @_;
1964 my $threshold = $column . '_threshold';
1965 $self->$column - $amount <= $self->$threshold + 0;
1967 '+' => sub { my($self, $column, $amount) = @_;
1968 ($self->$column || 0) + $amount > 0;
1973 my( $self, $op, $column, $amount ) = @_;
1975 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1976 ' ('. $self->email. "): $op $amount\n"
1979 return '' unless $amount;
1981 local $SIG{HUP} = 'IGNORE';
1982 local $SIG{INT} = 'IGNORE';
1983 local $SIG{QUIT} = 'IGNORE';
1984 local $SIG{TERM} = 'IGNORE';
1985 local $SIG{TSTP} = 'IGNORE';
1986 local $SIG{PIPE} = 'IGNORE';
1988 my $oldAutoCommit = $FS::UID::AutoCommit;
1989 local $FS::UID::AutoCommit = 0;
1992 my $sql = "UPDATE svc_acct SET $column = ".
1993 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1994 " $op ? WHERE svcnum = ?";
1998 my $sth = $dbh->prepare( $sql )
1999 or die "Error preparing $sql: ". $dbh->errstr;
2000 my $rv = $sth->execute($amount, $self->svcnum);
2001 die "Error executing $sql: ". $sth->errstr
2002 unless defined($rv);
2003 die "Can't update $column for svcnum". $self->svcnum
2006 #$self->snapshot; #not necessary, we retain the old values
2007 #create an object with the updated usage values
2008 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2010 my $error = $new->replace($self);
2012 $dbh->rollback if $oldAutoCommit;
2013 return "Error replacing: $error";
2016 #overlimit_action eq 'cancel' handling
2017 my $cust_pkg = $self->cust_svc->cust_pkg;
2019 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
2020 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
2024 my $error = $cust_pkg->cancel; #XXX should have a reason
2026 $dbh->rollback if $oldAutoCommit;
2027 return "Error cancelling: $error";
2030 #nothing else is relevant if we're cancelling, so commit & return success
2031 warn "$me update successful; committing\n"
2033 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2038 my $action = $op2action{$op};
2040 if ( &{$op2condition{$op}}($self, $column, $amount) &&
2041 ( $action eq 'suspend' && !$self->overlimit
2042 || $action eq 'unsuspend' && $self->overlimit )
2045 my $error = $self->_op_overlimit($action);
2047 $dbh->rollback if $oldAutoCommit;
2053 if ( $conf->exists("svc_acct-usage_$action")
2054 && &{$op2condition{$op}}($self, $column, $amount) ) {
2055 #my $error = $self->$action();
2056 my $error = $self->cust_svc->cust_pkg->$action();
2057 # $error ||= $self->overlimit($action);
2059 $dbh->rollback if $oldAutoCommit;
2060 return "Error ${action}ing: $error";
2064 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
2065 my $wqueue = new FS::queue {
2066 'svcnum' => $self->svcnum,
2067 'job' => 'FS::svc_acct::reached_threshold',
2072 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
2076 my $error = $wqueue->insert(
2077 'svcnum' => $self->svcnum,
2079 'column' => $column,
2083 $dbh->rollback if $oldAutoCommit;
2084 return "Error queuing threshold activity: $error";
2088 warn "$me update successful; committing\n"
2090 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2096 my( $self, $action ) = @_;
2098 local $SIG{HUP} = 'IGNORE';
2099 local $SIG{INT} = 'IGNORE';
2100 local $SIG{QUIT} = 'IGNORE';
2101 local $SIG{TERM} = 'IGNORE';
2102 local $SIG{TSTP} = 'IGNORE';
2103 local $SIG{PIPE} = 'IGNORE';
2105 my $oldAutoCommit = $FS::UID::AutoCommit;
2106 local $FS::UID::AutoCommit = 0;
2109 my $cust_pkg = $self->cust_svc->cust_pkg;
2111 my $conf_overlimit =
2113 ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
2114 : $conf->config('overlimit_groups');
2116 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
2118 my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
2119 next unless $groups;
2121 my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
2123 my $other = new FS::svc_acct $self->hashref;
2124 $other->usergroup( $gref );
2127 if ($action eq 'suspend') {
2130 } else { # $action eq 'unsuspend'
2135 my $error = $part_export->export_replace($new, $old)
2136 || $self->overlimit($action);
2139 $dbh->rollback if $oldAutoCommit;
2140 return "Error replacing radius groups: $error";
2145 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2151 my( $self, $valueref, %options ) = @_;
2153 warn "$me set_usage called for svcnum ". $self->svcnum.
2154 ' ('. $self->email. "): ".
2155 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
2158 local $SIG{HUP} = 'IGNORE';
2159 local $SIG{INT} = 'IGNORE';
2160 local $SIG{QUIT} = 'IGNORE';
2161 local $SIG{TERM} = 'IGNORE';
2162 local $SIG{TSTP} = 'IGNORE';
2163 local $SIG{PIPE} = 'IGNORE';
2165 local $FS::svc_Common::noexport_hack = 1;
2166 my $oldAutoCommit = $FS::UID::AutoCommit;
2167 local $FS::UID::AutoCommit = 0;
2172 if ( $options{null} ) {
2173 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
2174 qw( seconds upbytes downbytes totalbytes )
2177 foreach my $field (keys %$valueref){
2178 $reset = 1 if $valueref->{$field};
2179 $self->setfield($field, $valueref->{$field});
2180 $self->setfield( $field.'_threshold',
2181 int($self->getfield($field)
2182 * ( $conf->exists('svc_acct-usage_threshold')
2183 ? 1 - $conf->config('svc_acct-usage_threshold')/100
2188 $handyhash{$field} = $self->getfield($field);
2189 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
2191 #my $error = $self->replace; #NO! we avoid the call to ->check for
2192 #die $error if $error; #services not explicity changed via the UI
2194 my $sql = "UPDATE svc_acct SET " .
2195 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
2196 " WHERE svcnum = ". $self->svcnum;
2201 if (scalar(keys %handyhash)) {
2202 my $sth = $dbh->prepare( $sql )
2203 or die "Error preparing $sql: ". $dbh->errstr;
2204 my $rv = $sth->execute();
2205 die "Error executing $sql: ". $sth->errstr
2206 unless defined($rv);
2207 die "Can't update usage for svcnum ". $self->svcnum
2211 #$self->snapshot; #not necessary, we retain the old values
2212 #create an object with the updated usage values
2213 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2214 local($FS::Record::nowarn_identical) = 1;
2215 my $error = $new->replace($self); #call exports
2217 $dbh->rollback if $oldAutoCommit;
2218 return "Error replacing: $error";
2225 $error = $self->_op_overlimit('unsuspend')
2226 if $self->overlimit;;
2228 $error ||= $self->cust_svc->cust_pkg->unsuspend
2229 if $conf->exists("svc_acct-usage_unsuspend");
2232 $dbh->rollback if $oldAutoCommit;
2233 return "Error unsuspending: $error";
2238 warn "$me update successful; committing\n"
2240 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2246 =item recharge HASHREF
2248 Increments usage columns by the amount specified in HASHREF as
2249 column=>amount pairs.
2254 my ($self, $vhash) = @_;
2257 warn "[$me] recharge called on $self: ". Dumper($self).
2258 "\nwith vhash: ". Dumper($vhash);
2261 my $oldAutoCommit = $FS::UID::AutoCommit;
2262 local $FS::UID::AutoCommit = 0;
2266 foreach my $column (keys %$vhash){
2267 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
2271 $dbh->rollback if $oldAutoCommit;
2273 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2278 =item is_rechargeable
2280 Returns true if this svc_account can be "recharged" and false otherwise.
2284 sub is_rechargable {
2286 $self->seconds ne ''
2287 || $self->upbytes ne ''
2288 || $self->downbytes ne ''
2289 || $self->totalbytes ne '';
2292 =item seconds_since TIMESTAMP
2294 Returns the number of seconds this account has been online since TIMESTAMP,
2295 according to the session monitor (see L<FS::Session>).
2297 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2298 L<Time::Local> and L<Date::Parse> for conversion functions.
2302 #note: POD here, implementation in FS::cust_svc
2305 $self->cust_svc->seconds_since(@_);
2308 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
2310 Returns the numbers of seconds this account has been online between
2311 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
2312 external SQL radacct table, specified via sqlradius export. Sessions which
2313 started in the specified range but are still open are counted from session
2314 start to the end of the range (unless they are over 1 day old, in which case
2315 they are presumed missing their stop record and not counted). Also, sessions
2316 which end in the range but started earlier are counted from the start of the
2317 range to session end. Finally, sessions which start before the range but end
2318 after are counted for the entire range.
2320 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2321 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2326 #note: POD here, implementation in FS::cust_svc
2327 sub seconds_since_sqlradacct {
2329 $self->cust_svc->seconds_since_sqlradacct(@_);
2332 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2334 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2335 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2336 TIMESTAMP_END (exclusive).
2338 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2339 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2344 #note: POD here, implementation in FS::cust_svc
2345 sub attribute_since_sqlradacct {
2347 $self->cust_svc->attribute_since_sqlradacct(@_);
2350 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2352 Returns an array of hash references of this customers login history for the
2353 given time range. (document this better)
2357 sub get_session_history {
2359 $self->cust_svc->get_session_history(@_);
2362 =item last_login_text
2364 Returns text describing the time of last login.
2368 sub last_login_text {
2370 $self->last_login ? ctime($self->last_login) : 'unknown';
2373 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2378 my($self, $start, $end, %opt ) = @_;
2380 my $did = $self->username; #yup
2382 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2384 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2386 #SELECT $for_update * FROM cdr
2387 # WHERE calldate >= $start #need a conversion
2388 # AND calldate < $end #ditto
2389 # AND ( charged_party = "$did"
2390 # OR charged_party = "$prefix$did" #if length($prefix);
2391 # OR ( ( charged_party IS NULL OR charged_party = '' )
2393 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2396 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2399 if ( length($prefix) ) {
2401 " AND ( charged_party = '$did'
2402 OR charged_party = '$prefix$did'
2403 OR ( ( charged_party IS NULL OR charged_party = '' )
2405 ( src = '$did' OR src = '$prefix$did' )
2411 " AND ( charged_party = '$did'
2412 OR ( ( charged_party IS NULL OR charged_party = '' )
2422 'select' => "$for_update *",
2425 #( freesidestatus IS NULL OR freesidestatus = '' )
2426 'freesidestatus' => '',
2428 'extra_sql' => $charged_or_src,
2436 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2442 if ( $self->usergroup ) {
2443 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2444 unless ref($self->usergroup) eq 'ARRAY';
2445 #when provisioning records, export callback runs in svc_Common.pm before
2446 #radius_usergroup records can be inserted...
2447 @{$self->usergroup};
2449 map { $_->groupname }
2450 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2454 =item clone_suspended
2456 Constructor used by FS::part_export::_export_suspend fallback. Document
2461 sub clone_suspended {
2463 my %hash = $self->hash;
2464 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2465 new FS::svc_acct \%hash;
2468 =item clone_kludge_unsuspend
2470 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2475 sub clone_kludge_unsuspend {
2477 my %hash = $self->hash;
2478 $hash{_password} = '';
2479 new FS::svc_acct \%hash;
2482 =item check_password
2484 Checks the supplied password against the (possibly encrypted) password in the
2485 database. Returns true for a successful authentication, false for no match.
2487 Currently supported encryptions are: classic DES crypt() and MD5
2491 sub check_password {
2492 my($self, $check_password) = @_;
2494 #remove old-style SUSPENDED kludge, they should be allowed to login to
2495 #self-service and pay up
2496 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2498 if ( $self->_password_encoding eq 'ldap' ) {
2500 my $auth = from_rfc2307 Authen::Passphrase $self->_password;
2501 return $auth->match($check_password);
2503 } elsif ( $self->_password_encoding eq 'crypt' ) {
2505 my $auth = from_crypt Authen::Passphrase $self->_password;
2506 return $auth->match($check_password);
2508 } elsif ( $self->_password_encoding eq 'plain' ) {
2510 return $check_password eq $password;
2514 #XXX this could be replaced with Authen::Passphrase stuff
2516 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2518 } elsif ( length($password) < 13 ) { #plaintext
2519 $check_password eq $password;
2520 } elsif ( length($password) == 13 ) { #traditional DES crypt
2521 crypt($check_password, $password) eq $password;
2522 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2523 unix_md5_crypt($check_password, $password) eq $password;
2524 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2525 warn "Can't check password: Blowfish encryption not yet supported, ".
2526 "svcnum ". $self->svcnum. "\n";
2529 warn "Can't check password: Unrecognized encryption for svcnum ".
2530 $self->svcnum. "\n";
2538 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2540 Returns an encrypted password, either by passing through an encrypted password
2541 in the database or by encrypting a plaintext password from the database.
2543 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2544 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2545 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2546 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2547 encryption type is only used if the password is not already encrypted in the
2552 sub crypt_password {
2555 if ( $self->_password_encoding eq 'ldap' ) {
2557 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2560 #XXX this could be replaced with Authen::Passphrase stuff
2562 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2563 if ( $encryption eq 'crypt' ) {
2566 $saltset[int(rand(64))].$saltset[int(rand(64))]
2568 } elsif ( $encryption eq 'md5' ) {
2569 unix_md5_crypt( $self->_password );
2570 } elsif ( $encryption eq 'blowfish' ) {
2571 croak "unknown encryption method $encryption";
2573 croak "unknown encryption method $encryption";
2576 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2580 } elsif ( $self->_password_encoding eq 'crypt' ) {
2582 return $self->_password;
2584 } elsif ( $self->_password_encoding eq 'plain' ) {
2586 #XXX this could be replaced with Authen::Passphrase stuff
2588 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2589 if ( $encryption eq 'crypt' ) {
2592 $saltset[int(rand(64))].$saltset[int(rand(64))]
2594 } elsif ( $encryption eq 'md5' ) {
2595 unix_md5_crypt( $self->_password );
2596 } elsif ( $encryption eq 'blowfish' ) {
2597 croak "unknown encryption method $encryption";
2599 croak "unknown encryption method $encryption";
2604 if ( length($self->_password) == 13
2605 || $self->_password =~ /^\$(1|2a?)\$/
2606 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2612 #XXX this could be replaced with Authen::Passphrase stuff
2614 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2615 if ( $encryption eq 'crypt' ) {
2618 $saltset[int(rand(64))].$saltset[int(rand(64))]
2620 } elsif ( $encryption eq 'md5' ) {
2621 unix_md5_crypt( $self->_password );
2622 } elsif ( $encryption eq 'blowfish' ) {
2623 croak "unknown encryption method $encryption";
2625 croak "unknown encryption method $encryption";
2634 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2636 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2637 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2638 "{MD5}5426824942db4253f87a1009fd5d2d4".
2640 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2641 to work the same as the B</crypt_password> method.
2647 #eventually should check a "password-encoding" field
2649 if ( $self->_password_encoding eq 'ldap' ) {
2651 return $self->_password;
2653 } elsif ( $self->_password_encoding eq 'crypt' ) {
2655 if ( length($self->_password) == 13 ) { #crypt
2656 return '{CRYPT}'. $self->_password;
2657 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2659 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2660 # die "Blowfish encryption not supported in this context, svcnum ".
2661 # $self->svcnum. "\n";
2663 warn "encryption method not (yet?) supported in LDAP context";
2664 return '{CRYPT}*'; #unsupported, should not auth
2667 } elsif ( $self->_password_encoding eq 'plain' ) {
2669 return '{PLAIN}'. $self->_password;
2671 #return '{CLEARTEXT}'. $self->_password; #?
2675 if ( length($self->_password) == 13 ) { #crypt
2676 return '{CRYPT}'. $self->_password;
2677 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2679 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2680 warn "Blowfish encryption not supported in this context, svcnum ".
2681 $self->svcnum. "\n";
2684 #are these two necessary anymore?
2685 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2686 return '{SSHA}'. $1;
2687 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2688 return '{NS-MTA-MD5}'. $1;
2691 return '{PLAIN}'. $self->_password;
2693 #return '{CLEARTEXT}'. $self->_password; #?
2695 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2696 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2697 #if ( $encryption eq 'crypt' ) {
2698 # return '{CRYPT}'. crypt(
2700 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2702 #} elsif ( $encryption eq 'md5' ) {
2703 # unix_md5_crypt( $self->_password );
2704 #} elsif ( $encryption eq 'blowfish' ) {
2705 # croak "unknown encryption method $encryption";
2707 # croak "unknown encryption method $encryption";
2715 =item domain_slash_username
2717 Returns $domain/$username/
2721 sub domain_slash_username {
2723 $self->domain. '/'. $self->username. '/';
2726 =item virtual_maildir
2728 Returns $domain/maildirs/$username/
2732 sub virtual_maildir {
2734 $self->domain. '/maildirs/'. $self->username. '/';
2739 =head1 CLASS METHODS
2743 =item search HASHREF
2745 Class method which returns a qsearch hash expression to search for parameters
2746 specified in HASHREF. Valid parameters are
2760 Arrayref of pkgparts
2766 Arrayref of additional WHERE clauses, will be ANDed together.
2777 my ($class, $params) = @_;
2782 if ( $params->{'domain'} ) {
2783 my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
2784 #preserve previous behavior & bubble up an error if $svc_domain not found?
2785 push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
2789 if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
2790 push @where, "domsvc = $1";
2794 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
2797 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
2798 push @where, "agentnum = $1";
2802 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
2803 push @where, "custnum = $1";
2807 if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
2808 #XXX untaint or sql quote
2810 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
2814 if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
2815 push @where, "popnum = $1";
2819 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
2820 push @where, "svcpart = $1";
2824 # here is the agent virtualization
2825 #if ($params->{CurrentUser}) {
2827 # qsearchs('access_user', { username => $params->{CurrentUser} });
2829 # if ($access_user) {
2830 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
2832 # push @where, "1=0";
2835 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
2836 'table' => 'cust_main',
2837 'null_right' => 'View/link unlinked services',
2841 push @where, @{ $params->{'where'} } if $params->{'where'};
2843 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
2845 my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
2846 ' LEFT JOIN part_svc USING ( svcpart ) '.
2847 ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
2848 ' LEFT JOIN cust_main USING ( custnum ) ';
2850 my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
2851 #if ( keys %svc_acct ) {
2852 # $count_query .= ' WHERE '.
2853 # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
2859 'table' => 'svc_acct',
2860 'hashref' => {}, # \%svc_acct,
2861 'select' => join(', ',
2864 'cust_main.custnum',
2865 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
2867 'addl_from' => $addl_from,
2868 'extra_sql' => $extra_sql,
2869 'order_by' => $params->{'order_by'},
2870 'count_query' => $count_query,
2883 This is the FS::svc_acct job-queue-able version. It still uses
2884 FS::Misc::send_email under-the-hood.
2891 eval "use FS::Misc qw(send_email)";
2894 $opt{mimetype} ||= 'text/plain';
2895 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2897 my $error = send_email(
2898 'from' => $opt{from},
2900 'subject' => $opt{subject},
2901 'content-type' => $opt{mimetype},
2902 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2904 die $error if $error;
2907 =item check_and_rebuild_fuzzyfiles
2911 sub check_and_rebuild_fuzzyfiles {
2912 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2913 -e "$dir/svc_acct.username"
2914 or &rebuild_fuzzyfiles;
2917 =item rebuild_fuzzyfiles
2921 sub rebuild_fuzzyfiles {
2923 use Fcntl qw(:flock);
2925 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2929 open(USERNAMELOCK,">>$dir/svc_acct.username")
2930 or die "can't open $dir/svc_acct.username: $!";
2931 flock(USERNAMELOCK,LOCK_EX)
2932 or die "can't lock $dir/svc_acct.username: $!";
2934 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2936 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2937 or die "can't open $dir/svc_acct.username.tmp: $!";
2938 print USERNAMECACHE join("\n", @all_username), "\n";
2939 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2941 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2951 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2952 open(USERNAMECACHE,"<$dir/svc_acct.username")
2953 or die "can't open $dir/svc_acct.username: $!";
2954 my @array = map { chomp; $_; } <USERNAMECACHE>;
2955 close USERNAMECACHE;
2959 =item append_fuzzyfiles USERNAME
2963 sub append_fuzzyfiles {
2964 my $username = shift;
2966 &check_and_rebuild_fuzzyfiles;
2968 use Fcntl qw(:flock);
2970 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2972 open(USERNAME,">>$dir/svc_acct.username")
2973 or die "can't open $dir/svc_acct.username: $!";
2974 flock(USERNAME,LOCK_EX)
2975 or die "can't lock $dir/svc_acct.username: $!";
2977 print USERNAME "$username\n";
2979 flock(USERNAME,LOCK_UN)
2980 or die "can't unlock $dir/svc_acct.username: $!";
2988 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2992 sub radius_usergroup_selector {
2993 my $sel_groups = shift;
2994 my %sel_groups = map { $_=>1 } @$sel_groups;
2996 my $selectname = shift || 'radius_usergroup';
2999 my $sth = $dbh->prepare(
3000 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
3001 ) or die $dbh->errstr;
3002 $sth->execute() or die $sth->errstr;
3003 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
3007 function ${selectname}_doadd(object) {
3008 var myvalue = object.${selectname}_add.value;
3009 var optionName = new Option(myvalue,myvalue,false,true);
3010 var length = object.$selectname.length;
3011 object.$selectname.options[length] = optionName;
3012 object.${selectname}_add.value = "";
3015 <SELECT MULTIPLE NAME="$selectname">
3018 foreach my $group ( @all_groups ) {
3019 $html .= qq(<OPTION VALUE="$group");
3020 if ( $sel_groups{$group} ) {
3021 $html .= ' SELECTED';
3022 $sel_groups{$group} = 0;
3024 $html .= ">$group</OPTION>\n";
3026 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
3027 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
3029 $html .= '</SELECT>';
3031 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
3032 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
3037 =item reached_threshold
3039 Performs some activities when svc_acct thresholds (such as number of seconds
3040 remaining) are reached.
3044 sub reached_threshold {
3047 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
3048 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
3050 if ( $opt{'op'} eq '+' ){
3051 $svc_acct->setfield( $opt{'column'}.'_threshold',
3052 int($svc_acct->getfield($opt{'column'})
3053 * ( $conf->exists('svc_acct-usage_threshold')
3054 ? $conf->config('svc_acct-usage_threshold')/100
3059 my $error = $svc_acct->replace;
3060 die $error if $error;
3061 }elsif ( $opt{'op'} eq '-' ){
3063 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
3064 return '' if ($threshold eq '' );
3066 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
3067 my $error = $svc_acct->replace;
3068 die $error if $error; # email next time, i guess
3070 if ( $warning_template ) {
3071 eval "use FS::Misc qw(send_email)";
3074 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
3075 my $cust_main = $cust_pkg->cust_main;
3077 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
3078 $cust_main->invoicing_list,
3079 ($opt{'to'} ? $opt{'to'} : ())
3082 my $mimetype = $warning_mimetype;
3083 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
3085 my $body = $warning_template->fill_in( HASH => {
3086 'custnum' => $cust_main->custnum,
3087 'username' => $svc_acct->username,
3088 'password' => $svc_acct->_password,
3089 'first' => $cust_main->first,
3090 'last' => $cust_main->getfield('last'),
3091 'pkg' => $cust_pkg->part_pkg->pkg,
3092 'column' => $opt{'column'},
3093 'amount' => $opt{'column'} =~/bytes/
3094 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
3095 : $svc_acct->getfield($opt{'column'}),
3096 'threshold' => $opt{'column'} =~/bytes/
3097 ? FS::UI::bytecount::display_bytecount($threshold)
3102 my $error = send_email(
3103 'from' => $warning_from,
3105 'subject' => $warning_subject,
3106 'content-type' => $mimetype,
3107 'body' => [ map "$_\n", split("\n", $body) ],
3109 die $error if $error;
3112 die "unknown op: " . $opt{'op'};
3120 The $recref stuff in sub check should be cleaned up.
3122 The suspend, unsuspend and cancel methods update the database, but not the
3123 current object. This is probably a bug as it's unexpected and
3126 radius_usergroup_selector? putting web ui components in here? they should
3127 probably live somewhere else...
3129 insertion of RADIUS group stuff in insert could be done with child_objects now
3130 (would probably clean up export of them too)
3132 _op_usage and set_usage bypass the history... maybe they shouldn't
3136 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
3137 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
3138 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
3139 L<freeside-queued>), L<FS::svc_acct_pop>,
3140 schema.html from the base documentation.
3144 =item domain_select_hash %OPTIONS
3146 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
3147 may at present purchase.
3149 Currently available options are: I<pkgnum> I<svcpart>
3153 sub domain_select_hash {
3154 my ($self, %options) = @_;
3160 $part_svc = $self->part_svc;
3161 $cust_pkg = $self->cust_svc->cust_pkg
3165 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
3166 if $options{'svcpart'};
3168 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
3169 if $options{'pkgnum'};
3171 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
3172 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
3173 %domains = map { $_->svcnum => $_->domain }
3174 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
3175 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
3176 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
3177 %domains = map { $_->svcnum => $_->domain }
3178 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
3179 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
3180 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
3182 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
3185 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
3186 my $svc_domain = qsearchs('svc_domain',
3187 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
3188 if ( $svc_domain ) {
3189 $domains{$svc_domain->svcnum} = $svc_domain->domain;
3191 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
3192 $part_svc->part_svc_column('domsvc')->columnvalue;