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,
277 disable_inventory => 1,
281 label => 'File storage limit',
283 disable_inventory => 1,
287 label => 'Number of files limit',
289 disable_inventory => 1,
293 label => 'File size limit',
295 disable_inventory => 1,
298 '_password' => 'Password',
301 def_info => 'when blank, defaults to UID',
306 def_info => 'set to blank for no shell tracking',
308 #select_list => [ $conf->config('shells') ],
309 select_list => [ $conf ? $conf->config('shells') : () ],
310 disable_inventory => 1,
313 'finger' => 'Real name', # (GECOS)',
317 select_table => 'svc_domain',
318 select_key => 'svcnum',
319 select_label => 'domain',
320 disable_inventory => 1,
326 select_table => 'svc_domain',
327 select_key => 'svcnum',
328 select_label => 'domain',
329 disable_inventory => 1,
332 'pbxsvc' => { label => 'PBX',
333 type => 'select-svc_pbx.html',
334 disable_inventory => 1,
335 disable_select => 1, #UI wonky, pry works otherwise
338 label => 'RADIUS groups',
339 type => 'radius_usergroup_selector',
340 disable_inventory => 1,
343 'seconds' => { label => 'Seconds',
344 label_sort => 'with Time Remaining',
346 disable_inventory => 1,
348 disable_part_svc_column => 1,
350 'upbytes' => { label => 'Upload',
352 disable_inventory => 1,
354 'format' => \&FS::UI::bytecount::display_bytecount,
355 'parse' => \&FS::UI::bytecount::parse_bytecount,
356 disable_part_svc_column => 1,
358 'downbytes' => { label => 'Download',
360 disable_inventory => 1,
362 'format' => \&FS::UI::bytecount::display_bytecount,
363 'parse' => \&FS::UI::bytecount::parse_bytecount,
364 disable_part_svc_column => 1,
366 'totalbytes'=> { label => 'Total up and download',
368 disable_inventory => 1,
370 'format' => \&FS::UI::bytecount::display_bytecount,
371 'parse' => \&FS::UI::bytecount::parse_bytecount,
372 disable_part_svc_column => 1,
374 'seconds_threshold' => { label => 'Seconds threshold',
376 disable_inventory => 1,
378 disable_part_svc_column => 1,
380 'upbytes_threshold' => { label => 'Upload threshold',
382 disable_inventory => 1,
384 'format' => \&FS::UI::bytecount::display_bytecount,
385 'parse' => \&FS::UI::bytecount::parse_bytecount,
386 disable_part_svc_column => 1,
388 'downbytes_threshold' => { label => 'Download threshold',
390 disable_inventory => 1,
392 'format' => \&FS::UI::bytecount::display_bytecount,
393 'parse' => \&FS::UI::bytecount::parse_bytecount,
394 disable_part_svc_column => 1,
396 'totalbytes_threshold'=> { label => 'Total up and download threshold',
398 disable_inventory => 1,
400 'format' => \&FS::UI::bytecount::display_bytecount,
401 'parse' => \&FS::UI::bytecount::parse_bytecount,
402 disable_part_svc_column => 1,
405 label => 'Last login',
409 label => 'Last logout',
416 sub table { 'svc_acct'; }
418 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
422 #false laziness with edit/svc_acct.cgi
424 my( $self, $groups ) = @_;
425 if ( ref($groups) eq 'ARRAY' ) {
427 } elsif ( length($groups) ) {
428 [ split(/\s*,\s*/, $groups) ];
437 shift->_lastlog('in', @_);
441 shift->_lastlog('out', @_);
445 my( $self, $op, $time ) = @_;
447 if ( defined($time) ) {
448 warn "$me last_log$op called on svcnum ". $self->svcnum.
449 ' ('. $self->email. "): $time\n"
454 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
458 my $sth = $dbh->prepare( $sql )
459 or die "Error preparing $sql: ". $dbh->errstr;
460 my $rv = $sth->execute($time, $self->svcnum);
461 die "Error executing $sql: ". $sth->errstr
463 die "Can't update last_log$op for svcnum". $self->svcnum
466 $self->{'Hash'}->{"last_log$op"} = $time;
468 $self->getfield("last_log$op");
472 =item search_sql STRING
474 Class method which returns an SQL fragment to search for the given string.
479 my( $class, $string ) = @_;
480 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
481 my( $username, $domain ) = ( $1, $2 );
482 my $q_username = dbh->quote($username);
483 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
485 "svc_acct.username = $q_username AND ( ".
486 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
491 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
493 $class->search_sql_field('slipip', $string ).
495 $class->search_sql_field('username', $string ).
498 $class->search_sql_field('username', $string);
502 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
504 Returns the "username@domain" string for this account.
506 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
516 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
518 Returns a longer string label for this acccount ("Real Name <username@domain>"
519 if available, or "username@domain").
521 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
528 my $label = $self->label(@_);
529 my $finger = $self->finger;
530 return $label unless $finger =~ /\S/;
531 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
532 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
536 =item insert [ , OPTION => VALUE ... ]
538 Adds this account to the database. If there is an error, returns the error,
539 otherwise returns false.
541 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
542 defined. An FS::cust_svc record will be created and inserted.
544 The additional field I<usergroup> can optionally be defined; if so it should
545 contain an arrayref of group names. See L<FS::radius_usergroup>.
547 The additional field I<child_objects> can optionally be defined; if so it
548 should contain an arrayref of FS::tablename objects. They will have their
549 svcnum fields set and will be inserted after this record, but before any
550 exports are run. Each element of the array can also optionally be a
551 two-element array reference containing the child object and the name of an
552 alternate field to be filled in with the newly-inserted svcnum, for example
553 C<[ $svc_forward, 'srcsvc' ]>
555 Currently available options are: I<depend_jobnum>
557 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
558 jobnums), all provisioning jobs will have a dependancy on the supplied
559 jobnum(s) (they will not run until the specific job(s) complete(s)).
561 (TODOC: L<FS::queue> and L<freeside-queued>)
563 (TODOC: new exports!)
572 warn "[$me] insert called on $self: ". Dumper($self).
573 "\nwith options: ". Dumper(%options);
576 local $SIG{HUP} = 'IGNORE';
577 local $SIG{INT} = 'IGNORE';
578 local $SIG{QUIT} = 'IGNORE';
579 local $SIG{TERM} = 'IGNORE';
580 local $SIG{TSTP} = 'IGNORE';
581 local $SIG{PIPE} = 'IGNORE';
583 my $oldAutoCommit = $FS::UID::AutoCommit;
584 local $FS::UID::AutoCommit = 0;
588 my $error = $self->SUPER::insert(
589 'jobnums' => \@jobnums,
590 'child_objects' => $self->child_objects,
594 $dbh->rollback if $oldAutoCommit;
598 if ( $self->usergroup ) {
599 foreach my $groupname ( @{$self->usergroup} ) {
600 my $radius_usergroup = new FS::radius_usergroup ( {
601 svcnum => $self->svcnum,
602 groupname => $groupname,
604 my $error = $radius_usergroup->insert;
606 $dbh->rollback if $oldAutoCommit;
612 unless ( $skip_fuzzyfiles ) {
613 $error = $self->queue_fuzzyfiles_update;
615 $dbh->rollback if $oldAutoCommit;
616 return "updating fuzzy search cache: $error";
620 my $cust_pkg = $self->cust_svc->cust_pkg;
623 my $cust_main = $cust_pkg->cust_main;
624 my $agentnum = $cust_main->agentnum;
626 if ( $conf->exists('emailinvoiceautoalways')
627 || $conf->exists('emailinvoiceauto')
628 && ! $cust_main->invoicing_list_emailonly
630 my @invoicing_list = $cust_main->invoicing_list;
631 push @invoicing_list, $self->email;
632 $cust_main->invoicing_list(\@invoicing_list);
636 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
637 = ('','','','','','');
639 if ( $conf->exists('welcome_email', $agentnum) ) {
640 $welcome_template = new Text::Template (
642 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
643 ) or warn "can't create welcome email template: $Text::Template::ERROR";
644 $welcome_from = $conf->config('welcome_email-from', $agentnum);
645 # || 'your-isp-is-dum'
646 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
648 $welcome_subject_template = new Text::Template (
650 SOURCE => $welcome_subject,
651 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
652 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
655 if ( $welcome_template && $cust_pkg ) {
656 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
660 'custnum' => $self->custnum,
661 'username' => $self->username,
662 'password' => $self->_password,
663 'first' => $cust_main->first,
664 'last' => $cust_main->getfield('last'),
665 'pkg' => $cust_pkg->part_pkg->pkg,
667 my $wqueue = new FS::queue {
668 'svcnum' => $self->svcnum,
669 'job' => 'FS::svc_acct::send_email'
671 my $error = $wqueue->insert(
673 'from' => $welcome_from,
674 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
675 'mimetype' => $welcome_mimetype,
676 'body' => $welcome_template->fill_in( HASH => \%hash, ),
679 $dbh->rollback if $oldAutoCommit;
680 return "error queuing welcome email: $error";
683 if ( $options{'depend_jobnum'} ) {
684 warn "$me depend_jobnum found; adding to welcome email dependancies"
686 if ( ref($options{'depend_jobnum'}) ) {
687 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
688 "to welcome email dependancies"
690 push @jobnums, @{ $options{'depend_jobnum'} };
692 warn "$me adding job $options{'depend_jobnum'} ".
693 "to welcome email dependancies"
695 push @jobnums, $options{'depend_jobnum'};
699 foreach my $jobnum ( @jobnums ) {
700 my $error = $wqueue->depend_insert($jobnum);
702 $dbh->rollback if $oldAutoCommit;
703 return "error queuing welcome email job dependancy: $error";
713 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
717 # set usage fields and thresholds if unset but set in a package def
718 # AND the package already has a last bill date (otherwise they get double added)
719 sub preinsert_hook_first {
722 return '' unless $self->pkgnum;
724 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
725 return '' unless $cust_pkg && $cust_pkg->last_bill;
727 my $part_pkg = $cust_pkg->part_pkg;
728 return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
730 my %values = $part_pkg->usage_valuehash;
731 my $multiplier = $conf->exists('svc_acct-usage_threshold')
732 ? 1 - $conf->config('svc_acct-usage_threshold')/100
733 : 0.20; #doesn't matter
735 foreach ( keys %values ) {
736 next if $self->getfield($_);
737 $self->setfield( $_, $values{$_} );
738 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
739 if $conf->exists('svc_acct-usage_threshold');
747 Deletes this account from the database. If there is an error, returns the
748 error, otherwise returns false.
750 The corresponding FS::cust_svc record will be deleted as well.
752 (TODOC: new exports!)
759 return "can't delete system account" if $self->_check_system;
761 return "Can't delete an account which is a (svc_forward) source!"
762 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
764 return "Can't delete an account which is a (svc_forward) destination!"
765 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
767 return "Can't delete an account with (svc_www) web service!"
768 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
770 # what about records in session ? (they should refer to history table)
772 local $SIG{HUP} = 'IGNORE';
773 local $SIG{INT} = 'IGNORE';
774 local $SIG{QUIT} = 'IGNORE';
775 local $SIG{TERM} = 'IGNORE';
776 local $SIG{TSTP} = 'IGNORE';
777 local $SIG{PIPE} = 'IGNORE';
779 my $oldAutoCommit = $FS::UID::AutoCommit;
780 local $FS::UID::AutoCommit = 0;
783 foreach my $cust_main_invoice (
784 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
786 unless ( defined($cust_main_invoice) ) {
787 warn "WARNING: something's wrong with qsearch";
790 my %hash = $cust_main_invoice->hash;
791 $hash{'dest'} = $self->email;
792 my $new = new FS::cust_main_invoice \%hash;
793 my $error = $new->replace($cust_main_invoice);
795 $dbh->rollback if $oldAutoCommit;
800 foreach my $svc_domain (
801 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
803 my %hash = new FS::svc_domain->hash;
804 $hash{'catchall'} = '';
805 my $new = new FS::svc_domain \%hash;
806 my $error = $new->replace($svc_domain);
808 $dbh->rollback if $oldAutoCommit;
813 my $error = $self->SUPER::delete;
815 $dbh->rollback if $oldAutoCommit;
819 foreach my $radius_usergroup (
820 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
822 my $error = $radius_usergroup->delete;
824 $dbh->rollback if $oldAutoCommit;
829 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
833 =item replace OLD_RECORD
835 Replaces OLD_RECORD with this one in the database. If there is an error,
836 returns the error, otherwise returns false.
838 The additional field I<usergroup> can optionally be defined; if so it should
839 contain an arrayref of group names. See L<FS::radius_usergroup>.
847 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
851 warn "$me replacing $old with $new\n" if $DEBUG;
855 return "can't modify system account" if $old->_check_system;
858 #no warnings 'numeric'; #alas, a 5.006-ism
861 foreach my $xid (qw( uid gid )) {
863 return "Can't change $xid!"
864 if ! $conf->exists("svc_acct-edit_$xid")
865 && $old->$xid() != $new->$xid()
866 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
871 #change homdir when we change username
872 $new->setfield('dir', '') if $old->username ne $new->username;
874 local $SIG{HUP} = 'IGNORE';
875 local $SIG{INT} = 'IGNORE';
876 local $SIG{QUIT} = 'IGNORE';
877 local $SIG{TERM} = 'IGNORE';
878 local $SIG{TSTP} = 'IGNORE';
879 local $SIG{PIPE} = 'IGNORE';
881 my $oldAutoCommit = $FS::UID::AutoCommit;
882 local $FS::UID::AutoCommit = 0;
885 # redundant, but so $new->usergroup gets set
886 $error = $new->check;
887 return $error if $error;
889 $old->usergroup( [ $old->radius_groups ] );
891 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
892 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
894 if ( $new->usergroup ) {
895 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
896 my @newgroups = @{$new->usergroup};
897 foreach my $oldgroup ( @{$old->usergroup} ) {
898 if ( grep { $oldgroup eq $_ } @newgroups ) {
899 @newgroups = grep { $oldgroup ne $_ } @newgroups;
902 my $radius_usergroup = qsearchs('radius_usergroup', {
903 svcnum => $old->svcnum,
904 groupname => $oldgroup,
906 my $error = $radius_usergroup->delete;
908 $dbh->rollback if $oldAutoCommit;
909 return "error deleting radius_usergroup $oldgroup: $error";
913 foreach my $newgroup ( @newgroups ) {
914 my $radius_usergroup = new FS::radius_usergroup ( {
915 svcnum => $new->svcnum,
916 groupname => $newgroup,
918 my $error = $radius_usergroup->insert;
920 $dbh->rollback if $oldAutoCommit;
921 return "error adding radius_usergroup $newgroup: $error";
927 $error = $new->SUPER::replace($old, @_);
929 $dbh->rollback if $oldAutoCommit;
930 return $error if $error;
933 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
934 $error = $new->queue_fuzzyfiles_update;
936 $dbh->rollback if $oldAutoCommit;
937 return "updating fuzzy search cache: $error";
941 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
945 =item queue_fuzzyfiles_update
947 Used by insert & replace to update the fuzzy search cache
951 sub queue_fuzzyfiles_update {
954 local $SIG{HUP} = 'IGNORE';
955 local $SIG{INT} = 'IGNORE';
956 local $SIG{QUIT} = 'IGNORE';
957 local $SIG{TERM} = 'IGNORE';
958 local $SIG{TSTP} = 'IGNORE';
959 local $SIG{PIPE} = 'IGNORE';
961 my $oldAutoCommit = $FS::UID::AutoCommit;
962 local $FS::UID::AutoCommit = 0;
965 my $queue = new FS::queue {
966 'svcnum' => $self->svcnum,
967 'job' => 'FS::svc_acct::append_fuzzyfiles'
969 my $error = $queue->insert($self->username);
971 $dbh->rollback if $oldAutoCommit;
972 return "queueing job (transaction rolled back): $error";
975 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
983 Suspends this account by calling export-specific suspend hooks. If there is
984 an error, returns the error, otherwise returns false.
986 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
992 return "can't suspend system account" if $self->_check_system;
993 $self->SUPER::suspend(@_);
998 Unsuspends this account by by calling export-specific suspend hooks. If there
999 is an error, returns the error, otherwise returns false.
1001 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1007 my %hash = $self->hash;
1008 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
1009 $hash{_password} = $1;
1010 my $new = new FS::svc_acct ( \%hash );
1011 my $error = $new->replace($self);
1012 return $error if $error;
1015 $self->SUPER::unsuspend(@_);
1020 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1022 If the B<auto_unset_catchall> configuration option is set, this method will
1023 automatically remove any references to the canceled service in the catchall
1024 field of svc_domain. This allows packages that contain both a svc_domain and
1025 its catchall svc_acct to be canceled in one step.
1030 # Only one thing to do at this level
1032 foreach my $svc_domain (
1033 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
1034 if($conf->exists('auto_unset_catchall')) {
1035 my %hash = $svc_domain->hash;
1036 $hash{catchall} = '';
1037 my $new = new FS::svc_domain ( \%hash );
1038 my $error = $new->replace($svc_domain);
1039 return $error if $error;
1041 return "cannot unprovision svc_acct #".$self->svcnum.
1042 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
1046 $self->SUPER::cancel(@_);
1052 Checks all fields to make sure this is a valid service. If there is an error,
1053 returns the error, otherwise returns false. Called by the insert and replace
1056 Sets any fixed values; see L<FS::part_svc>.
1063 my($recref) = $self->hashref;
1065 my $x = $self->setfixed( $self->_fieldhandlers );
1066 return $x unless ref($x);
1069 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1071 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1074 my $error = $self->ut_numbern('svcnum')
1075 #|| $self->ut_number('domsvc')
1076 || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
1077 || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
1078 || $self->ut_textn('sec_phrase')
1079 || $self->ut_snumbern('seconds')
1080 || $self->ut_snumbern('upbytes')
1081 || $self->ut_snumbern('downbytes')
1082 || $self->ut_snumbern('totalbytes')
1083 || $self->ut_enum( '_password_encoding',
1084 [ '', qw( plain crypt ldap ) ]
1086 || $self->ut_enum( 'password_selfchange', [ '', 'Y' ] )
1087 || $self->ut_enum( 'password_recover', [ '', 'Y' ] )
1088 || $self->ut_alphasn( 'cgp_accessmodes' )
1089 || $self->ut_alphan( 'cgp_type' )
1091 return $error if $error;
1094 local $username_letter = $username_letter;
1095 if ($self->svcnum) {
1096 my $cust_svc = $self->cust_svc
1097 or return "no cust_svc record found for svcnum ". $self->svcnum;
1098 my $cust_pkg = $cust_svc->cust_pkg;
1100 if ($self->pkgnum) {
1101 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );#complain?
1105 $conf->exists('username-letter', $cust_pkg->cust_main->agentnum);
1108 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1109 if ( $username_uppercase ) {
1110 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1111 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1112 $recref->{username} = $1;
1114 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1115 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1116 $recref->{username} = $1;
1119 if ( $username_letterfirst ) {
1120 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1121 } elsif ( $username_letter ) {
1122 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1124 if ( $username_noperiod ) {
1125 $recref->{username} =~ /\./ and return gettext('illegal_username');
1127 if ( $username_nounderscore ) {
1128 $recref->{username} =~ /_/ and return gettext('illegal_username');
1130 if ( $username_nodash ) {
1131 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1133 unless ( $username_ampersand ) {
1134 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1136 unless ( $username_percent ) {
1137 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1139 unless ( $username_colon ) {
1140 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1143 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1144 $recref->{popnum} = $1;
1145 return "Unknown popnum" unless
1146 ! $recref->{popnum} ||
1147 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1149 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1151 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1152 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1154 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1155 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1156 #not all systems use gid=uid
1157 #you can set a fixed gid in part_svc
1159 return "Only root can have uid 0"
1160 if $recref->{uid} == 0
1161 && $recref->{username} !~ /^(root|toor|smtp)$/;
1163 unless ( $recref->{username} eq 'sync' ) {
1164 if ( grep $_ eq $recref->{shell}, @shells ) {
1165 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1167 return "Illegal shell \`". $self->shell. "\'; ".
1168 "shells configuration value contains: @shells";
1171 $recref->{shell} = '/bin/sync';
1175 $recref->{gid} ne '' ?
1176 return "Can't have gid without uid" : ( $recref->{gid}='' );
1177 #$recref->{dir} ne '' ?
1178 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1179 $recref->{shell} ne '' ?
1180 return "Can't have shell without uid" : ( $recref->{shell}='' );
1183 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1185 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1186 or return "Illegal directory: ". $recref->{dir};
1187 $recref->{dir} = $1;
1188 return "Illegal directory"
1189 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1190 return "Illegal directory"
1191 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1192 unless ( $recref->{dir} ) {
1193 $recref->{dir} = $dir_prefix . '/';
1194 if ( $dirhash > 0 ) {
1195 for my $h ( 1 .. $dirhash ) {
1196 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1198 } elsif ( $dirhash < 0 ) {
1199 for my $h ( reverse $dirhash .. -1 ) {
1200 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1203 $recref->{dir} .= $recref->{username};
1209 # $error = $self->ut_textn('finger');
1210 # return $error if $error;
1211 if ( $self->getfield('finger') eq '' ) {
1212 my $cust_pkg = $self->svcnum
1213 ? $self->cust_svc->cust_pkg
1214 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1216 my $cust_main = $cust_pkg->cust_main;
1217 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1220 $self->getfield('finger') =~
1221 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1222 or return "Illegal finger: ". $self->getfield('finger');
1223 $self->setfield('finger', $1);
1225 for (qw( quota file_quota file_maxsize )) {
1226 $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
1229 $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
1230 $recref->{file_maxnum} = $1;
1232 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1233 if ( $recref->{slipip} eq '' ) {
1234 $recref->{slipip} = '';
1235 } elsif ( $recref->{slipip} eq '0e0' ) {
1236 $recref->{slipip} = '0e0';
1238 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1239 or return "Illegal slipip: ". $self->slipip;
1240 $recref->{slipip} = $1;
1245 #arbitrary RADIUS stuff; allow ut_textn for now
1246 foreach ( grep /^radius_/, fields('svc_acct') ) {
1247 $self->ut_textn($_);
1250 # First, if _password is blank, generate one and set default encoding.
1251 if ( ! $recref->{_password} ) {
1252 $error = $self->set_password('');
1254 # But if there's a _password but no encoding, assume it's plaintext and
1255 # set it to default encoding.
1256 elsif ( ! $recref->{_password_encoding} ) {
1257 $error = $self->set_password($recref->{_password});
1259 return $error if $error;
1261 # Next, check _password to ensure compliance with the encoding.
1262 if ( $recref->{_password_encoding} eq 'ldap' ) {
1264 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1265 $recref->{_password} = uc($1).$2;
1267 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1270 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1272 if ( $recref->{_password} =~
1273 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1274 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1277 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1280 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1283 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1284 # Password randomization is now in set_password.
1285 # Strip whitespace characters, check length requirements, etc.
1286 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1287 $recref->{_password} = $1;
1289 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1290 FS::Msgcat::_gettext('illegal_password_characters').
1291 ": ". $recref->{_password};
1294 if ( $password_noampersand ) {
1295 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1297 if ( $password_noexclamation ) {
1298 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1302 return "invalid password encoding ('".$recref->{_password_encoding}."'";
1304 $self->SUPER::check;
1309 sub _password_encryption {
1311 my $encoding = lc($self->_password_encoding);
1312 return if !$encoding;
1313 return 'plain' if $encoding eq 'plain';
1314 if($encoding eq 'crypt') {
1315 my $pass = $self->_password;
1316 $pass =~ s/^\*SUSPENDED\* //;
1318 return 'md5' if $pass =~ /^\$1\$/;
1319 #return 'blowfish' if $self->_password =~ /^\$2\$/;
1320 return 'des' if length($pass) == 13;
1323 if($encoding eq 'ldap') {
1324 uc($self->_password) =~ /^\{([\w-]+)\}/;
1325 return 'crypt' if $1 eq 'CRYPT' or $1 eq 'DES';
1326 return 'plain' if $1 eq 'PLAIN' or $1 eq 'CLEARTEXT';
1327 return 'md5' if $1 eq 'MD5';
1328 return 'sha1' if $1 eq 'SHA' or $1 eq 'SHA-1';
1335 sub get_cleartext_password {
1337 if($self->_password_encryption eq 'plain') {
1338 if($self->_password_encoding eq 'ldap') {
1339 $self->_password =~ /\{\w+\}(.*)$/;
1343 return $self->_password;
1352 Set the cleartext password for the account. If _password_encoding is set, the
1353 new password will be encoded according to the existing method (including
1354 encryption mode, if it can be determined). Otherwise,
1355 config('default-password-encoding') is used.
1357 If no password is supplied (or a zero-length password when minimum password length
1358 is >0), one will be generated randomly.
1363 my( $self, $pass ) = ( shift, shift );
1365 warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
1368 my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
1369 FS::Msgcat::_gettext('illegal_password_characters').
1372 my( $encoding, $encryption ) = ('', '');
1374 if ( $self->_password_encoding ) {
1375 $encoding = $self->_password_encoding;
1376 # identify existing encryption method, try to use it.
1377 $encryption = $self->_password_encryption;
1379 # use the system default
1385 # set encoding to system default
1386 ($encoding, $encryption) =
1387 split(/-/, lc($conf->config('default-password-encoding')));
1388 $encoding ||= 'legacy';
1389 $self->_password_encoding($encoding);
1392 if ( $encoding eq 'legacy' ) {
1394 # The legacy behavior from check():
1395 # If the password is blank, randomize it and set encoding to 'plain'.
1396 if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
1397 $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1398 $self->_password_encoding('plain');
1400 # Prefix + valid-length password
1401 if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1403 $self->_password_encoding('plain');
1404 # Prefix + crypt string
1405 } elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1407 $self->_password_encoding('crypt');
1408 # Various disabled crypt passwords
1409 } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
1410 $self->_password_encoding('crypt');
1416 $self->_password($pass);
1422 if $passwordmin && length($pass) < $passwordmin
1423 or $passwordmax && length($pass) > $passwordmax;
1425 if ( $encoding eq 'crypt' ) {
1426 if ($encryption eq 'md5') {
1427 $pass = unix_md5_crypt($pass);
1428 } elsif ($encryption eq 'des') {
1429 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1432 } elsif ( $encoding eq 'ldap' ) {
1433 if ($encryption eq 'md5') {
1434 $pass = md5_base64($pass);
1435 } elsif ($encryption eq 'sha1') {
1436 $pass = sha1_base64($pass);
1437 } elsif ($encryption eq 'crypt') {
1438 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1440 # else $encryption eq 'plain', do nothing
1441 $pass = '{'.uc($encryption).'}'.$pass;
1443 # else encoding eq 'plain'
1445 $self->_password($pass);
1451 Internal function to check the username against the list of system usernames
1452 from the I<system_usernames> configuration value. Returns true if the username
1453 is listed on the system username list.
1459 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1460 $conf->config('system_usernames')
1464 =item _check_duplicate
1466 Internal method to check for duplicates usernames, username@domain pairs and
1469 If the I<global_unique-username> configuration value is set to B<username> or
1470 B<username@domain>, enforces global username or username@domain uniqueness.
1472 In all cases, check for duplicate uids and usernames or username@domain pairs
1473 per export and with identical I<svcpart> values.
1477 sub _check_duplicate {
1480 my $global_unique = $conf->config('global_unique-username') || 'none';
1481 return '' if $global_unique eq 'disabled';
1485 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1486 unless ( $part_svc ) {
1487 return 'unknown svcpart '. $self->svcpart;
1490 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1491 qsearch( 'svc_acct', { 'username' => $self->username } );
1492 return gettext('username_in_use')
1493 if $global_unique eq 'username' && @dup_user;
1495 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1496 qsearch( 'svc_acct', { 'username' => $self->username,
1497 'domsvc' => $self->domsvc } );
1498 return gettext('username_in_use')
1499 if $global_unique eq 'username@domain' && @dup_userdomain;
1502 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1503 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1504 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1505 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1510 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1511 my $exports = FS::part_export::export_info('svc_acct');
1512 my %conflict_user_svcpart;
1513 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1515 foreach my $part_export ( $part_svc->part_export ) {
1517 #this will catch to the same exact export
1518 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1520 #this will catch to exports w/same exporthost+type ???
1521 #my @other_part_export = qsearch('part_export', {
1522 # 'machine' => $part_export->machine,
1523 # 'exporttype' => $part_export->exporttype,
1525 #foreach my $other_part_export ( @other_part_export ) {
1526 # push @svcparts, map { $_->svcpart }
1527 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1530 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1531 #silly kludge to avoid uninitialized value errors
1532 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1533 ? $exports->{$part_export->exporttype}{'nodomain'}
1535 if ( $nodomain =~ /^Y/i ) {
1536 $conflict_user_svcpart{$_} = $part_export->exportnum
1539 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1544 foreach my $dup_user ( @dup_user ) {
1545 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1546 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1547 return "duplicate username ". $self->username.
1548 ": conflicts with svcnum ". $dup_user->svcnum.
1549 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1553 foreach my $dup_userdomain ( @dup_userdomain ) {
1554 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1555 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1556 return "duplicate username\@domain ". $self->email.
1557 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1558 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1562 foreach my $dup_uid ( @dup_uid ) {
1563 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1564 if ( exists($conflict_user_svcpart{$dup_svcpart})
1565 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1566 return "duplicate uid ". $self->uid.
1567 ": conflicts with svcnum ". $dup_uid->svcnum.
1569 ( $conflict_user_svcpart{$dup_svcpart}
1570 || $conflict_userdomain_svcpart{$dup_svcpart} );
1582 Depriciated, use radius_reply instead.
1587 carp "FS::svc_acct::radius depriciated, use radius_reply";
1588 $_[0]->radius_reply;
1593 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1594 reply attributes of this record.
1596 Note that this is now the preferred method for reading RADIUS attributes -
1597 accessing the columns directly is discouraged, as the column names are
1598 expected to change in the future.
1605 return %{ $self->{'radius_reply'} }
1606 if exists $self->{'radius_reply'};
1611 my($column, $attrib) = ($1, $2);
1612 #$attrib =~ s/_/\-/g;
1613 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1614 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1616 if ( $self->slipip && $self->slipip ne '0e0' ) {
1617 $reply{$radius_ip} = $self->slipip;
1620 if ( $self->seconds !~ /^$/ ) {
1621 $reply{'Session-Timeout'} = $self->seconds;
1624 if ( $conf->exists('radius-chillispot-max') ) {
1625 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1627 #hmm. just because sqlradius.pm says so?
1634 foreach my $what (qw( input output total )) {
1635 my $is = $whatis{$what}.'bytes';
1636 if ( $self->$is() =~ /\d/ ) {
1637 my $big = new Math::BigInt $self->$is();
1638 $big = new Math::BigInt '0' if $big->is_neg();
1639 my $att = "Chillispot-Max-\u$what";
1640 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1641 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1652 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1653 check attributes of this record.
1655 Note that this is now the preferred method for reading RADIUS attributes -
1656 accessing the columns directly is discouraged, as the column names are
1657 expected to change in the future.
1664 return %{ $self->{'radius_check'} }
1665 if exists $self->{'radius_check'};
1670 my($column, $attrib) = ($1, $2);
1671 #$attrib =~ s/_/\-/g;
1672 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1673 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1676 my($pw_attrib, $password) = $self->radius_password;
1677 $check{$pw_attrib} = $password;
1679 my $cust_svc = $self->cust_svc;
1681 my $cust_pkg = $cust_svc->cust_pkg;
1682 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1683 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1686 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1687 "; can't set Expiration\n"
1695 =item radius_password
1697 Returns a key/value pair containing the RADIUS attribute name and value
1702 sub radius_password {
1706 if ( $self->_password_encoding eq 'ldap' ) {
1707 $pw_attrib = 'Password-With-Header';
1708 } elsif ( $self->_password_encoding eq 'crypt' ) {
1709 $pw_attrib = 'Crypt-Password';
1710 } elsif ( $self->_password_encoding eq 'plain' ) {
1711 $pw_attrib = $radius_password;
1713 $pw_attrib = length($self->_password) <= 12
1718 ($pw_attrib, $self->_password);
1724 This method instructs the object to "snapshot" or freeze RADIUS check and
1725 reply attributes to the current values.
1729 #bah, my english is too broken this morning
1730 #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
1731 #the FS::cust_pkg's replace method to trigger the correct export updates when
1732 #package dates change)
1737 $self->{$_} = { $self->$_() }
1738 foreach qw( radius_reply radius_check );
1742 =item forget_snapshot
1744 This methos instructs the object to forget any previously snapshotted
1745 RADIUS check and reply attributes.
1749 sub forget_snapshot {
1753 foreach qw( radius_reply radius_check );
1757 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1759 Returns the domain associated with this account.
1761 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1768 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1769 my $svc_domain = $self->svc_domain(@_)
1770 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1771 $svc_domain->domain;
1776 Returns the FS::svc_domain record for this account's domain (see
1781 # FS::h_svc_acct has a history-aware svc_domain override
1786 ? $self->{'_domsvc'}
1787 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1792 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1796 #inherited from svc_Common
1798 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1800 Returns an email address associated with the account.
1802 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1809 $self->username. '@'. $self->domain(@_);
1814 Returns an array of FS::acct_snarf records associated with the account.
1815 If the acct_snarf table does not exist or there are no associated records,
1816 an empty list is returned
1822 return () unless dbdef->table('acct_snarf');
1823 eval "use FS::acct_snarf;";
1825 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1828 =item decrement_upbytes OCTETS
1830 Decrements the I<upbytes> field of this record by the given amount. If there
1831 is an error, returns the error, otherwise returns false.
1835 sub decrement_upbytes {
1836 shift->_op_usage('-', 'upbytes', @_);
1839 =item increment_upbytes OCTETS
1841 Increments the I<upbytes> field of this record by the given amount. If there
1842 is an error, returns the error, otherwise returns false.
1846 sub increment_upbytes {
1847 shift->_op_usage('+', 'upbytes', @_);
1850 =item decrement_downbytes OCTETS
1852 Decrements the I<downbytes> field of this record by the given amount. If there
1853 is an error, returns the error, otherwise returns false.
1857 sub decrement_downbytes {
1858 shift->_op_usage('-', 'downbytes', @_);
1861 =item increment_downbytes OCTETS
1863 Increments the I<downbytes> field of this record by the given amount. If there
1864 is an error, returns the error, otherwise returns false.
1868 sub increment_downbytes {
1869 shift->_op_usage('+', 'downbytes', @_);
1872 =item decrement_totalbytes OCTETS
1874 Decrements the I<totalbytes> field of this record by the given amount. If there
1875 is an error, returns the error, otherwise returns false.
1879 sub decrement_totalbytes {
1880 shift->_op_usage('-', 'totalbytes', @_);
1883 =item increment_totalbytes OCTETS
1885 Increments the I<totalbytes> field of this record by the given amount. If there
1886 is an error, returns the error, otherwise returns false.
1890 sub increment_totalbytes {
1891 shift->_op_usage('+', 'totalbytes', @_);
1894 =item decrement_seconds SECONDS
1896 Decrements the I<seconds> field of this record by the given amount. If there
1897 is an error, returns the error, otherwise returns false.
1901 sub decrement_seconds {
1902 shift->_op_usage('-', 'seconds', @_);
1905 =item increment_seconds SECONDS
1907 Increments the I<seconds> field of this record by the given amount. If there
1908 is an error, returns the error, otherwise returns false.
1912 sub increment_seconds {
1913 shift->_op_usage('+', 'seconds', @_);
1921 my %op2condition = (
1922 '-' => sub { my($self, $column, $amount) = @_;
1923 $self->$column - $amount <= 0;
1925 '+' => sub { my($self, $column, $amount) = @_;
1926 ($self->$column || 0) + $amount > 0;
1929 my %op2warncondition = (
1930 '-' => sub { my($self, $column, $amount) = @_;
1931 my $threshold = $column . '_threshold';
1932 $self->$column - $amount <= $self->$threshold + 0;
1934 '+' => sub { my($self, $column, $amount) = @_;
1935 ($self->$column || 0) + $amount > 0;
1940 my( $self, $op, $column, $amount ) = @_;
1942 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1943 ' ('. $self->email. "): $op $amount\n"
1946 return '' unless $amount;
1948 local $SIG{HUP} = 'IGNORE';
1949 local $SIG{INT} = 'IGNORE';
1950 local $SIG{QUIT} = 'IGNORE';
1951 local $SIG{TERM} = 'IGNORE';
1952 local $SIG{TSTP} = 'IGNORE';
1953 local $SIG{PIPE} = 'IGNORE';
1955 my $oldAutoCommit = $FS::UID::AutoCommit;
1956 local $FS::UID::AutoCommit = 0;
1959 my $sql = "UPDATE svc_acct SET $column = ".
1960 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1961 " $op ? WHERE svcnum = ?";
1965 my $sth = $dbh->prepare( $sql )
1966 or die "Error preparing $sql: ". $dbh->errstr;
1967 my $rv = $sth->execute($amount, $self->svcnum);
1968 die "Error executing $sql: ". $sth->errstr
1969 unless defined($rv);
1970 die "Can't update $column for svcnum". $self->svcnum
1973 #$self->snapshot; #not necessary, we retain the old values
1974 #create an object with the updated usage values
1975 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1977 my $error = $new->replace($self);
1979 $dbh->rollback if $oldAutoCommit;
1980 return "Error replacing: $error";
1983 #overlimit_action eq 'cancel' handling
1984 my $cust_pkg = $self->cust_svc->cust_pkg;
1986 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
1987 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
1991 my $error = $cust_pkg->cancel; #XXX should have a reason
1993 $dbh->rollback if $oldAutoCommit;
1994 return "Error cancelling: $error";
1997 #nothing else is relevant if we're cancelling, so commit & return success
1998 warn "$me update successful; committing\n"
2000 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2005 my $action = $op2action{$op};
2007 if ( &{$op2condition{$op}}($self, $column, $amount) &&
2008 ( $action eq 'suspend' && !$self->overlimit
2009 || $action eq 'unsuspend' && $self->overlimit )
2012 my $error = $self->_op_overlimit($action);
2014 $dbh->rollback if $oldAutoCommit;
2020 if ( $conf->exists("svc_acct-usage_$action")
2021 && &{$op2condition{$op}}($self, $column, $amount) ) {
2022 #my $error = $self->$action();
2023 my $error = $self->cust_svc->cust_pkg->$action();
2024 # $error ||= $self->overlimit($action);
2026 $dbh->rollback if $oldAutoCommit;
2027 return "Error ${action}ing: $error";
2031 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
2032 my $wqueue = new FS::queue {
2033 'svcnum' => $self->svcnum,
2034 'job' => 'FS::svc_acct::reached_threshold',
2039 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
2043 my $error = $wqueue->insert(
2044 'svcnum' => $self->svcnum,
2046 'column' => $column,
2050 $dbh->rollback if $oldAutoCommit;
2051 return "Error queuing threshold activity: $error";
2055 warn "$me update successful; committing\n"
2057 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2063 my( $self, $action ) = @_;
2065 local $SIG{HUP} = 'IGNORE';
2066 local $SIG{INT} = 'IGNORE';
2067 local $SIG{QUIT} = 'IGNORE';
2068 local $SIG{TERM} = 'IGNORE';
2069 local $SIG{TSTP} = 'IGNORE';
2070 local $SIG{PIPE} = 'IGNORE';
2072 my $oldAutoCommit = $FS::UID::AutoCommit;
2073 local $FS::UID::AutoCommit = 0;
2076 my $cust_pkg = $self->cust_svc->cust_pkg;
2078 my $conf_overlimit =
2080 ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
2081 : $conf->config('overlimit_groups');
2083 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
2085 my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
2086 next unless $groups;
2088 my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
2090 my $other = new FS::svc_acct $self->hashref;
2091 $other->usergroup( $gref );
2094 if ($action eq 'suspend') {
2097 } else { # $action eq 'unsuspend'
2102 my $error = $part_export->export_replace($new, $old)
2103 || $self->overlimit($action);
2106 $dbh->rollback if $oldAutoCommit;
2107 return "Error replacing radius groups: $error";
2112 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2118 my( $self, $valueref, %options ) = @_;
2120 warn "$me set_usage called for svcnum ". $self->svcnum.
2121 ' ('. $self->email. "): ".
2122 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
2125 local $SIG{HUP} = 'IGNORE';
2126 local $SIG{INT} = 'IGNORE';
2127 local $SIG{QUIT} = 'IGNORE';
2128 local $SIG{TERM} = 'IGNORE';
2129 local $SIG{TSTP} = 'IGNORE';
2130 local $SIG{PIPE} = 'IGNORE';
2132 local $FS::svc_Common::noexport_hack = 1;
2133 my $oldAutoCommit = $FS::UID::AutoCommit;
2134 local $FS::UID::AutoCommit = 0;
2139 if ( $options{null} ) {
2140 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
2141 qw( seconds upbytes downbytes totalbytes )
2144 foreach my $field (keys %$valueref){
2145 $reset = 1 if $valueref->{$field};
2146 $self->setfield($field, $valueref->{$field});
2147 $self->setfield( $field.'_threshold',
2148 int($self->getfield($field)
2149 * ( $conf->exists('svc_acct-usage_threshold')
2150 ? 1 - $conf->config('svc_acct-usage_threshold')/100
2155 $handyhash{$field} = $self->getfield($field);
2156 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
2158 #my $error = $self->replace; #NO! we avoid the call to ->check for
2159 #die $error if $error; #services not explicity changed via the UI
2161 my $sql = "UPDATE svc_acct SET " .
2162 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
2163 " WHERE svcnum = ". $self->svcnum;
2168 if (scalar(keys %handyhash)) {
2169 my $sth = $dbh->prepare( $sql )
2170 or die "Error preparing $sql: ". $dbh->errstr;
2171 my $rv = $sth->execute();
2172 die "Error executing $sql: ". $sth->errstr
2173 unless defined($rv);
2174 die "Can't update usage for svcnum ". $self->svcnum
2178 #$self->snapshot; #not necessary, we retain the old values
2179 #create an object with the updated usage values
2180 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2181 local($FS::Record::nowarn_identical) = 1;
2182 my $error = $new->replace($self); #call exports
2184 $dbh->rollback if $oldAutoCommit;
2185 return "Error replacing: $error";
2192 $error = $self->_op_overlimit('unsuspend')
2193 if $self->overlimit;;
2195 $error ||= $self->cust_svc->cust_pkg->unsuspend
2196 if $conf->exists("svc_acct-usage_unsuspend");
2199 $dbh->rollback if $oldAutoCommit;
2200 return "Error unsuspending: $error";
2205 warn "$me update successful; committing\n"
2207 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2213 =item recharge HASHREF
2215 Increments usage columns by the amount specified in HASHREF as
2216 column=>amount pairs.
2221 my ($self, $vhash) = @_;
2224 warn "[$me] recharge called on $self: ". Dumper($self).
2225 "\nwith vhash: ". Dumper($vhash);
2228 my $oldAutoCommit = $FS::UID::AutoCommit;
2229 local $FS::UID::AutoCommit = 0;
2233 foreach my $column (keys %$vhash){
2234 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
2238 $dbh->rollback if $oldAutoCommit;
2240 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2245 =item is_rechargeable
2247 Returns true if this svc_account can be "recharged" and false otherwise.
2251 sub is_rechargable {
2253 $self->seconds ne ''
2254 || $self->upbytes ne ''
2255 || $self->downbytes ne ''
2256 || $self->totalbytes ne '';
2259 =item seconds_since TIMESTAMP
2261 Returns the number of seconds this account has been online since TIMESTAMP,
2262 according to the session monitor (see L<FS::Session>).
2264 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2265 L<Time::Local> and L<Date::Parse> for conversion functions.
2269 #note: POD here, implementation in FS::cust_svc
2272 $self->cust_svc->seconds_since(@_);
2275 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
2277 Returns the numbers of seconds this account has been online between
2278 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
2279 external SQL radacct table, specified via sqlradius export. Sessions which
2280 started in the specified range but are still open are counted from session
2281 start to the end of the range (unless they are over 1 day old, in which case
2282 they are presumed missing their stop record and not counted). Also, sessions
2283 which end in the range but started earlier are counted from the start of the
2284 range to session end. Finally, sessions which start before the range but end
2285 after are counted for the entire range.
2287 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2288 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2293 #note: POD here, implementation in FS::cust_svc
2294 sub seconds_since_sqlradacct {
2296 $self->cust_svc->seconds_since_sqlradacct(@_);
2299 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2301 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2302 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2303 TIMESTAMP_END (exclusive).
2305 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2306 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2311 #note: POD here, implementation in FS::cust_svc
2312 sub attribute_since_sqlradacct {
2314 $self->cust_svc->attribute_since_sqlradacct(@_);
2317 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2319 Returns an array of hash references of this customers login history for the
2320 given time range. (document this better)
2324 sub get_session_history {
2326 $self->cust_svc->get_session_history(@_);
2329 =item last_login_text
2331 Returns text describing the time of last login.
2335 sub last_login_text {
2337 $self->last_login ? ctime($self->last_login) : 'unknown';
2340 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2345 my($self, $start, $end, %opt ) = @_;
2347 my $did = $self->username; #yup
2349 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2351 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2353 #SELECT $for_update * FROM cdr
2354 # WHERE calldate >= $start #need a conversion
2355 # AND calldate < $end #ditto
2356 # AND ( charged_party = "$did"
2357 # OR charged_party = "$prefix$did" #if length($prefix);
2358 # OR ( ( charged_party IS NULL OR charged_party = '' )
2360 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2363 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2366 if ( length($prefix) ) {
2368 " AND ( charged_party = '$did'
2369 OR charged_party = '$prefix$did'
2370 OR ( ( charged_party IS NULL OR charged_party = '' )
2372 ( src = '$did' OR src = '$prefix$did' )
2378 " AND ( charged_party = '$did'
2379 OR ( ( charged_party IS NULL OR charged_party = '' )
2389 'select' => "$for_update *",
2392 #( freesidestatus IS NULL OR freesidestatus = '' )
2393 'freesidestatus' => '',
2395 'extra_sql' => $charged_or_src,
2403 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2409 if ( $self->usergroup ) {
2410 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2411 unless ref($self->usergroup) eq 'ARRAY';
2412 #when provisioning records, export callback runs in svc_Common.pm before
2413 #radius_usergroup records can be inserted...
2414 @{$self->usergroup};
2416 map { $_->groupname }
2417 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2421 =item clone_suspended
2423 Constructor used by FS::part_export::_export_suspend fallback. Document
2428 sub clone_suspended {
2430 my %hash = $self->hash;
2431 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2432 new FS::svc_acct \%hash;
2435 =item clone_kludge_unsuspend
2437 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2442 sub clone_kludge_unsuspend {
2444 my %hash = $self->hash;
2445 $hash{_password} = '';
2446 new FS::svc_acct \%hash;
2449 =item check_password
2451 Checks the supplied password against the (possibly encrypted) password in the
2452 database. Returns true for a successful authentication, false for no match.
2454 Currently supported encryptions are: classic DES crypt() and MD5
2458 sub check_password {
2459 my($self, $check_password) = @_;
2461 #remove old-style SUSPENDED kludge, they should be allowed to login to
2462 #self-service and pay up
2463 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2465 if ( $self->_password_encoding eq 'ldap' ) {
2467 my $auth = from_rfc2307 Authen::Passphrase $self->_password;
2468 return $auth->match($check_password);
2470 } elsif ( $self->_password_encoding eq 'crypt' ) {
2472 my $auth = from_crypt Authen::Passphrase $self->_password;
2473 return $auth->match($check_password);
2475 } elsif ( $self->_password_encoding eq 'plain' ) {
2477 return $check_password eq $password;
2481 #XXX this could be replaced with Authen::Passphrase stuff
2483 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2485 } elsif ( length($password) < 13 ) { #plaintext
2486 $check_password eq $password;
2487 } elsif ( length($password) == 13 ) { #traditional DES crypt
2488 crypt($check_password, $password) eq $password;
2489 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2490 unix_md5_crypt($check_password, $password) eq $password;
2491 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2492 warn "Can't check password: Blowfish encryption not yet supported, ".
2493 "svcnum ". $self->svcnum. "\n";
2496 warn "Can't check password: Unrecognized encryption for svcnum ".
2497 $self->svcnum. "\n";
2505 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2507 Returns an encrypted password, either by passing through an encrypted password
2508 in the database or by encrypting a plaintext password from the database.
2510 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2511 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2512 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2513 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2514 encryption type is only used if the password is not already encrypted in the
2519 sub crypt_password {
2522 if ( $self->_password_encoding eq 'ldap' ) {
2524 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2527 #XXX this could be replaced with Authen::Passphrase stuff
2529 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2530 if ( $encryption eq 'crypt' ) {
2533 $saltset[int(rand(64))].$saltset[int(rand(64))]
2535 } elsif ( $encryption eq 'md5' ) {
2536 unix_md5_crypt( $self->_password );
2537 } elsif ( $encryption eq 'blowfish' ) {
2538 croak "unknown encryption method $encryption";
2540 croak "unknown encryption method $encryption";
2543 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2547 } elsif ( $self->_password_encoding eq 'crypt' ) {
2549 return $self->_password;
2551 } elsif ( $self->_password_encoding eq 'plain' ) {
2553 #XXX this could be replaced with Authen::Passphrase stuff
2555 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2556 if ( $encryption eq 'crypt' ) {
2559 $saltset[int(rand(64))].$saltset[int(rand(64))]
2561 } elsif ( $encryption eq 'md5' ) {
2562 unix_md5_crypt( $self->_password );
2563 } elsif ( $encryption eq 'blowfish' ) {
2564 croak "unknown encryption method $encryption";
2566 croak "unknown encryption method $encryption";
2571 if ( length($self->_password) == 13
2572 || $self->_password =~ /^\$(1|2a?)\$/
2573 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2579 #XXX this could be replaced with Authen::Passphrase stuff
2581 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2582 if ( $encryption eq 'crypt' ) {
2585 $saltset[int(rand(64))].$saltset[int(rand(64))]
2587 } elsif ( $encryption eq 'md5' ) {
2588 unix_md5_crypt( $self->_password );
2589 } elsif ( $encryption eq 'blowfish' ) {
2590 croak "unknown encryption method $encryption";
2592 croak "unknown encryption method $encryption";
2601 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2603 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2604 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2605 "{MD5}5426824942db4253f87a1009fd5d2d4".
2607 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2608 to work the same as the B</crypt_password> method.
2614 #eventually should check a "password-encoding" field
2616 if ( $self->_password_encoding eq 'ldap' ) {
2618 return $self->_password;
2620 } elsif ( $self->_password_encoding eq 'crypt' ) {
2622 if ( length($self->_password) == 13 ) { #crypt
2623 return '{CRYPT}'. $self->_password;
2624 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2626 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2627 # die "Blowfish encryption not supported in this context, svcnum ".
2628 # $self->svcnum. "\n";
2630 warn "encryption method not (yet?) supported in LDAP context";
2631 return '{CRYPT}*'; #unsupported, should not auth
2634 } elsif ( $self->_password_encoding eq 'plain' ) {
2636 return '{PLAIN}'. $self->_password;
2638 #return '{CLEARTEXT}'. $self->_password; #?
2642 if ( length($self->_password) == 13 ) { #crypt
2643 return '{CRYPT}'. $self->_password;
2644 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2646 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2647 warn "Blowfish encryption not supported in this context, svcnum ".
2648 $self->svcnum. "\n";
2651 #are these two necessary anymore?
2652 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2653 return '{SSHA}'. $1;
2654 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2655 return '{NS-MTA-MD5}'. $1;
2658 return '{PLAIN}'. $self->_password;
2660 #return '{CLEARTEXT}'. $self->_password; #?
2662 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2663 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2664 #if ( $encryption eq 'crypt' ) {
2665 # return '{CRYPT}'. crypt(
2667 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2669 #} elsif ( $encryption eq 'md5' ) {
2670 # unix_md5_crypt( $self->_password );
2671 #} elsif ( $encryption eq 'blowfish' ) {
2672 # croak "unknown encryption method $encryption";
2674 # croak "unknown encryption method $encryption";
2682 =item domain_slash_username
2684 Returns $domain/$username/
2688 sub domain_slash_username {
2690 $self->domain. '/'. $self->username. '/';
2693 =item virtual_maildir
2695 Returns $domain/maildirs/$username/
2699 sub virtual_maildir {
2701 $self->domain. '/maildirs/'. $self->username. '/';
2706 =head1 CLASS METHODS
2710 =item search HASHREF
2712 Class method which returns a qsearch hash expression to search for parameters
2713 specified in HASHREF. Valid parameters are
2727 Arrayref of pkgparts
2733 Arrayref of additional WHERE clauses, will be ANDed together.
2744 my ($class, $params) = @_;
2749 if ( $params->{'domain'} ) {
2750 my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
2751 #preserve previous behavior & bubble up an error if $svc_domain not found?
2752 push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
2756 if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
2757 push @where, "domsvc = $1";
2761 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
2764 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
2765 push @where, "agentnum = $1";
2769 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
2770 push @where, "custnum = $1";
2774 if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
2775 #XXX untaint or sql quote
2777 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
2781 if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
2782 push @where, "popnum = $1";
2786 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
2787 push @where, "svcpart = $1";
2791 # here is the agent virtualization
2792 #if ($params->{CurrentUser}) {
2794 # qsearchs('access_user', { username => $params->{CurrentUser} });
2796 # if ($access_user) {
2797 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
2799 # push @where, "1=0";
2802 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
2803 'table' => 'cust_main',
2804 'null_right' => 'View/link unlinked services',
2808 push @where, @{ $params->{'where'} } if $params->{'where'};
2810 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
2812 my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
2813 ' LEFT JOIN part_svc USING ( svcpart ) '.
2814 ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
2815 ' LEFT JOIN cust_main USING ( custnum ) ';
2817 my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
2818 #if ( keys %svc_acct ) {
2819 # $count_query .= ' WHERE '.
2820 # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
2826 'table' => 'svc_acct',
2827 'hashref' => {}, # \%svc_acct,
2828 'select' => join(', ',
2831 'cust_main.custnum',
2832 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
2834 'addl_from' => $addl_from,
2835 'extra_sql' => $extra_sql,
2836 'order_by' => $params->{'order_by'},
2837 'count_query' => $count_query,
2850 This is the FS::svc_acct job-queue-able version. It still uses
2851 FS::Misc::send_email under-the-hood.
2858 eval "use FS::Misc qw(send_email)";
2861 $opt{mimetype} ||= 'text/plain';
2862 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2864 my $error = send_email(
2865 'from' => $opt{from},
2867 'subject' => $opt{subject},
2868 'content-type' => $opt{mimetype},
2869 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2871 die $error if $error;
2874 =item check_and_rebuild_fuzzyfiles
2878 sub check_and_rebuild_fuzzyfiles {
2879 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2880 -e "$dir/svc_acct.username"
2881 or &rebuild_fuzzyfiles;
2884 =item rebuild_fuzzyfiles
2888 sub rebuild_fuzzyfiles {
2890 use Fcntl qw(:flock);
2892 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2896 open(USERNAMELOCK,">>$dir/svc_acct.username")
2897 or die "can't open $dir/svc_acct.username: $!";
2898 flock(USERNAMELOCK,LOCK_EX)
2899 or die "can't lock $dir/svc_acct.username: $!";
2901 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2903 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2904 or die "can't open $dir/svc_acct.username.tmp: $!";
2905 print USERNAMECACHE join("\n", @all_username), "\n";
2906 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2908 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2918 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2919 open(USERNAMECACHE,"<$dir/svc_acct.username")
2920 or die "can't open $dir/svc_acct.username: $!";
2921 my @array = map { chomp; $_; } <USERNAMECACHE>;
2922 close USERNAMECACHE;
2926 =item append_fuzzyfiles USERNAME
2930 sub append_fuzzyfiles {
2931 my $username = shift;
2933 &check_and_rebuild_fuzzyfiles;
2935 use Fcntl qw(:flock);
2937 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2939 open(USERNAME,">>$dir/svc_acct.username")
2940 or die "can't open $dir/svc_acct.username: $!";
2941 flock(USERNAME,LOCK_EX)
2942 or die "can't lock $dir/svc_acct.username: $!";
2944 print USERNAME "$username\n";
2946 flock(USERNAME,LOCK_UN)
2947 or die "can't unlock $dir/svc_acct.username: $!";
2955 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2959 sub radius_usergroup_selector {
2960 my $sel_groups = shift;
2961 my %sel_groups = map { $_=>1 } @$sel_groups;
2963 my $selectname = shift || 'radius_usergroup';
2966 my $sth = $dbh->prepare(
2967 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2968 ) or die $dbh->errstr;
2969 $sth->execute() or die $sth->errstr;
2970 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2974 function ${selectname}_doadd(object) {
2975 var myvalue = object.${selectname}_add.value;
2976 var optionName = new Option(myvalue,myvalue,false,true);
2977 var length = object.$selectname.length;
2978 object.$selectname.options[length] = optionName;
2979 object.${selectname}_add.value = "";
2982 <SELECT MULTIPLE NAME="$selectname">
2985 foreach my $group ( @all_groups ) {
2986 $html .= qq(<OPTION VALUE="$group");
2987 if ( $sel_groups{$group} ) {
2988 $html .= ' SELECTED';
2989 $sel_groups{$group} = 0;
2991 $html .= ">$group</OPTION>\n";
2993 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2994 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2996 $html .= '</SELECT>';
2998 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2999 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
3004 =item reached_threshold
3006 Performs some activities when svc_acct thresholds (such as number of seconds
3007 remaining) are reached.
3011 sub reached_threshold {
3014 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
3015 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
3017 if ( $opt{'op'} eq '+' ){
3018 $svc_acct->setfield( $opt{'column'}.'_threshold',
3019 int($svc_acct->getfield($opt{'column'})
3020 * ( $conf->exists('svc_acct-usage_threshold')
3021 ? $conf->config('svc_acct-usage_threshold')/100
3026 my $error = $svc_acct->replace;
3027 die $error if $error;
3028 }elsif ( $opt{'op'} eq '-' ){
3030 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
3031 return '' if ($threshold eq '' );
3033 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
3034 my $error = $svc_acct->replace;
3035 die $error if $error; # email next time, i guess
3037 if ( $warning_template ) {
3038 eval "use FS::Misc qw(send_email)";
3041 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
3042 my $cust_main = $cust_pkg->cust_main;
3044 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
3045 $cust_main->invoicing_list,
3046 ($opt{'to'} ? $opt{'to'} : ())
3049 my $mimetype = $warning_mimetype;
3050 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
3052 my $body = $warning_template->fill_in( HASH => {
3053 'custnum' => $cust_main->custnum,
3054 'username' => $svc_acct->username,
3055 'password' => $svc_acct->_password,
3056 'first' => $cust_main->first,
3057 'last' => $cust_main->getfield('last'),
3058 'pkg' => $cust_pkg->part_pkg->pkg,
3059 'column' => $opt{'column'},
3060 'amount' => $opt{'column'} =~/bytes/
3061 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
3062 : $svc_acct->getfield($opt{'column'}),
3063 'threshold' => $opt{'column'} =~/bytes/
3064 ? FS::UI::bytecount::display_bytecount($threshold)
3069 my $error = send_email(
3070 'from' => $warning_from,
3072 'subject' => $warning_subject,
3073 'content-type' => $mimetype,
3074 'body' => [ map "$_\n", split("\n", $body) ],
3076 die $error if $error;
3079 die "unknown op: " . $opt{'op'};
3087 The $recref stuff in sub check should be cleaned up.
3089 The suspend, unsuspend and cancel methods update the database, but not the
3090 current object. This is probably a bug as it's unexpected and
3093 radius_usergroup_selector? putting web ui components in here? they should
3094 probably live somewhere else...
3096 insertion of RADIUS group stuff in insert could be done with child_objects now
3097 (would probably clean up export of them too)
3099 _op_usage and set_usage bypass the history... maybe they shouldn't
3103 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
3104 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
3105 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
3106 L<freeside-queued>), L<FS::svc_acct_pop>,
3107 schema.html from the base documentation.
3111 =item domain_select_hash %OPTIONS
3113 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
3114 may at present purchase.
3116 Currently available options are: I<pkgnum> I<svcpart>
3120 sub domain_select_hash {
3121 my ($self, %options) = @_;
3127 $part_svc = $self->part_svc;
3128 $cust_pkg = $self->cust_svc->cust_pkg
3132 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
3133 if $options{'svcpart'};
3135 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
3136 if $options{'pkgnum'};
3138 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
3139 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
3140 %domains = map { $_->svcnum => $_->domain }
3141 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
3142 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
3143 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
3144 %domains = map { $_->svcnum => $_->domain }
3145 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
3146 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
3147 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
3149 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
3152 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
3153 my $svc_domain = qsearchs('svc_domain',
3154 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
3155 if ( $svc_domain ) {
3156 $domains{$svc_domain->svcnum} = $svc_domain->domain;
3158 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
3159 $part_svc->part_svc_column('domsvc')->columnvalue;