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;
25 use Authen::Passphrase;
26 use FS::UID qw( datasrc driver_name );
28 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
29 use FS::Msgcat qw(gettext);
30 use FS::UI::bytecount;
37 use FS::cust_main_invoice;
41 use FS::radius_usergroup;
48 @ISA = qw( FS::svc_Common );
51 $me = '[FS::svc_acct]';
53 #ask FS::UID to run this stuff for us later
54 FS::UID->install_callback( sub {
56 $dir_prefix = $conf->config('home');
57 @shells = $conf->config('shells');
58 $usernamemin = $conf->config('usernamemin') || 2;
59 $usernamemax = $conf->config('usernamemax');
60 $passwordmin = $conf->config('passwordmin'); # || 6;
62 $passwordmin = ( defined($passwordmin) && $passwordmin =~ /\d+/ )
65 $passwordmax = $conf->config('passwordmax') || 8;
66 $username_letter = $conf->exists('username-letter');
67 $username_letterfirst = $conf->exists('username-letterfirst');
68 $username_noperiod = $conf->exists('username-noperiod');
69 $username_nounderscore = $conf->exists('username-nounderscore');
70 $username_nodash = $conf->exists('username-nodash');
71 $username_uppercase = $conf->exists('username-uppercase');
72 $username_ampersand = $conf->exists('username-ampersand');
73 $username_percent = $conf->exists('username-percent');
74 $username_colon = $conf->exists('username-colon');
75 $password_noampersand = $conf->exists('password-noexclamation');
76 $password_noexclamation = $conf->exists('password-noexclamation');
77 $dirhash = $conf->config('dirhash') || 0;
78 if ( $conf->exists('warning_email') ) {
79 $warning_template = new Text::Template (
81 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
82 ) or warn "can't create warning email template: $Text::Template::ERROR";
83 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
84 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
85 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
86 $warning_cc = $conf->config('warning_email-cc');
88 $warning_template = '';
90 $warning_subject = '';
91 $warning_mimetype = '';
94 $smtpmachine = $conf->config('smtpmachine');
95 $radius_password = $conf->config('radius-password') || 'Password';
96 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
97 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
101 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
102 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
106 my ( $hashref, $cache ) = @_;
107 if ( $hashref->{'svc_acct_svcnum'} ) {
108 $self->{'_domsvc'} = FS::svc_domain->new( {
109 'svcnum' => $hashref->{'domsvc'},
110 'domain' => $hashref->{'svc_acct_domain'},
111 'catchall' => $hashref->{'svc_acct_catchall'},
118 FS::svc_acct - Object methods for svc_acct records
124 $record = new FS::svc_acct \%hash;
125 $record = new FS::svc_acct { 'column' => 'value' };
127 $error = $record->insert;
129 $error = $new_record->replace($old_record);
131 $error = $record->delete;
133 $error = $record->check;
135 $error = $record->suspend;
137 $error = $record->unsuspend;
139 $error = $record->cancel;
141 %hash = $record->radius;
143 %hash = $record->radius_reply;
145 %hash = $record->radius_check;
147 $domain = $record->domain;
149 $svc_domain = $record->svc_domain;
151 $email = $record->email;
153 $seconds_since = $record->seconds_since($timestamp);
157 An FS::svc_acct object represents an account. FS::svc_acct inherits from
158 FS::svc_Common. The following fields are currently supported:
162 =item svcnum - primary key (assigned automatcially for new accounts)
166 =item _password - generated if blank
168 =item _password_encoding - plain, crypt, ldap (or empty for autodetection)
170 =item sec_phrase - security phrase
172 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
180 =item dir - set automatically if blank (and uid is not)
184 =item quota - (unimplementd)
186 =item slipip - IP address
196 =item domsvc - svcnum from svc_domain
198 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
200 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
210 Creates a new account. To add the account to the database, see L<"insert">.
217 'longname_plural' => 'Access accounts and mailboxes',
218 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
219 'display_weight' => 10,
220 'cancel_weight' => 50,
222 'dir' => 'Home directory',
225 def_info => 'set to fixed and blank for no UIDs',
228 'slipip' => 'IP address',
229 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
231 label => 'Access number',
233 select_table => 'svc_acct_pop',
234 select_key => 'popnum',
235 select_label => 'city',
241 disable_default => 1,
248 disable_inventory => 1,
251 '_password' => 'Password',
254 def_info => 'when blank, defaults to UID',
259 def_info => 'set to blank for no shell tracking',
261 #select_list => [ $conf->config('shells') ],
262 select_list => [ $conf ? $conf->config('shells') : () ],
263 disable_inventory => 1,
266 'finger' => 'Real name', # (GECOS)',
270 select_table => 'svc_domain',
271 select_key => 'svcnum',
272 select_label => 'domain',
273 disable_inventory => 1,
277 label => 'RADIUS groups',
278 type => 'radius_usergroup_selector',
279 disable_inventory => 1,
282 'seconds' => { label => 'Seconds',
283 label_sort => 'with Time Remaining',
285 disable_inventory => 1,
287 disable_part_svc_column => 1,
289 'upbytes' => { label => 'Upload',
291 disable_inventory => 1,
293 'format' => \&FS::UI::bytecount::display_bytecount,
294 'parse' => \&FS::UI::bytecount::parse_bytecount,
295 disable_part_svc_column => 1,
297 'downbytes' => { label => 'Download',
299 disable_inventory => 1,
301 'format' => \&FS::UI::bytecount::display_bytecount,
302 'parse' => \&FS::UI::bytecount::parse_bytecount,
303 disable_part_svc_column => 1,
305 'totalbytes'=> { label => 'Total up and download',
307 disable_inventory => 1,
309 'format' => \&FS::UI::bytecount::display_bytecount,
310 'parse' => \&FS::UI::bytecount::parse_bytecount,
311 disable_part_svc_column => 1,
313 'seconds_threshold' => { label => 'Seconds threshold',
315 disable_inventory => 1,
317 disable_part_svc_column => 1,
319 'upbytes_threshold' => { label => 'Upload threshold',
321 disable_inventory => 1,
323 'format' => \&FS::UI::bytecount::display_bytecount,
324 'parse' => \&FS::UI::bytecount::parse_bytecount,
325 disable_part_svc_column => 1,
327 'downbytes_threshold' => { label => 'Download threshold',
329 disable_inventory => 1,
331 'format' => \&FS::UI::bytecount::display_bytecount,
332 'parse' => \&FS::UI::bytecount::parse_bytecount,
333 disable_part_svc_column => 1,
335 'totalbytes_threshold'=> { label => 'Total up and download threshold',
337 disable_inventory => 1,
339 'format' => \&FS::UI::bytecount::display_bytecount,
340 'parse' => \&FS::UI::bytecount::parse_bytecount,
341 disable_part_svc_column => 1,
344 label => 'Last login',
348 label => 'Last logout',
355 sub table { 'svc_acct'; }
357 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
361 #false laziness with edit/svc_acct.cgi
363 my( $self, $groups ) = @_;
364 if ( ref($groups) eq 'ARRAY' ) {
366 } elsif ( length($groups) ) {
367 [ split(/\s*,\s*/, $groups) ];
376 shift->_lastlog('in', @_);
380 shift->_lastlog('out', @_);
384 my( $self, $op, $time ) = @_;
386 if ( defined($time) ) {
387 warn "$me last_log$op called on svcnum ". $self->svcnum.
388 ' ('. $self->email. "): $time\n"
393 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
397 my $sth = $dbh->prepare( $sql )
398 or die "Error preparing $sql: ". $dbh->errstr;
399 my $rv = $sth->execute($time, $self->svcnum);
400 die "Error executing $sql: ". $sth->errstr
402 die "Can't update last_log$op for svcnum". $self->svcnum
405 $self->{'Hash'}->{"last_log$op"} = $time;
407 $self->getfield("last_log$op");
411 =item search_sql STRING
413 Class method which returns an SQL fragment to search for the given string.
418 my( $class, $string ) = @_;
419 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
420 my( $username, $domain ) = ( $1, $2 );
421 my $q_username = dbh->quote($username);
422 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
424 "svc_acct.username = $q_username AND ( ".
425 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
430 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
432 $class->search_sql_field('slipip', $string ).
434 $class->search_sql_field('username', $string ).
438 $class->search_sql_field('username', $string).
440 ? 'OR '. $class->search_sql_field('svcnum', $string)
447 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
449 Returns the "username@domain" string for this account.
451 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
461 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
463 Returns a longer string label for this acccount ("Real Name <username@domain>"
464 if available, or "username@domain").
466 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
473 my $label = $self->label(@_);
474 my $finger = $self->finger;
475 return $label unless $finger =~ /\S/;
476 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
477 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
481 =item insert [ , OPTION => VALUE ... ]
483 Adds this account to the database. If there is an error, returns the error,
484 otherwise returns false.
486 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
487 defined. An FS::cust_svc record will be created and inserted.
489 The additional field I<usergroup> can optionally be defined; if so it should
490 contain an arrayref of group names. See L<FS::radius_usergroup>.
492 The additional field I<child_objects> can optionally be defined; if so it
493 should contain an arrayref of FS::tablename objects. They will have their
494 svcnum fields set and will be inserted after this record, but before any
495 exports are run. Each element of the array can also optionally be a
496 two-element array reference containing the child object and the name of an
497 alternate field to be filled in with the newly-inserted svcnum, for example
498 C<[ $svc_forward, 'srcsvc' ]>
500 Currently available options are: I<depend_jobnum>
502 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
503 jobnums), all provisioning jobs will have a dependancy on the supplied
504 jobnum(s) (they will not run until the specific job(s) complete(s)).
506 (TODOC: L<FS::queue> and L<freeside-queued>)
508 (TODOC: new exports!)
517 warn "[$me] insert called on $self: ". Dumper($self).
518 "\nwith options: ". Dumper(%options);
521 local $SIG{HUP} = 'IGNORE';
522 local $SIG{INT} = 'IGNORE';
523 local $SIG{QUIT} = 'IGNORE';
524 local $SIG{TERM} = 'IGNORE';
525 local $SIG{TSTP} = 'IGNORE';
526 local $SIG{PIPE} = 'IGNORE';
528 my $oldAutoCommit = $FS::UID::AutoCommit;
529 local $FS::UID::AutoCommit = 0;
533 my $error = $self->SUPER::insert(
534 'jobnums' => \@jobnums,
535 'child_objects' => $self->child_objects,
539 $dbh->rollback if $oldAutoCommit;
543 if ( $self->usergroup ) {
544 foreach my $groupname ( @{$self->usergroup} ) {
545 my $radius_usergroup = new FS::radius_usergroup ( {
546 svcnum => $self->svcnum,
547 groupname => $groupname,
549 my $error = $radius_usergroup->insert;
551 $dbh->rollback if $oldAutoCommit;
557 unless ( $skip_fuzzyfiles ) {
558 $error = $self->queue_fuzzyfiles_update;
560 $dbh->rollback if $oldAutoCommit;
561 return "updating fuzzy search cache: $error";
565 my $cust_pkg = $self->cust_svc->cust_pkg;
568 my $cust_main = $cust_pkg->cust_main;
569 my $agentnum = $cust_main->agentnum;
571 if ( $conf->exists('emailinvoiceautoalways')
572 || $conf->exists('emailinvoiceauto')
573 && ! $cust_main->invoicing_list_emailonly
575 my @invoicing_list = $cust_main->invoicing_list;
576 push @invoicing_list, $self->email;
577 $cust_main->invoicing_list(\@invoicing_list);
581 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
582 = ('','','','','','');
584 if ( $conf->exists('welcome_email', $agentnum) ) {
585 $welcome_template = new Text::Template (
587 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
588 ) or warn "can't create welcome email template: $Text::Template::ERROR";
589 $welcome_from = $conf->config('welcome_email-from', $agentnum);
590 # || 'your-isp-is-dum'
591 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
593 $welcome_subject_template = new Text::Template (
595 SOURCE => $welcome_subject,
596 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
597 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
600 if ( $welcome_template && $cust_pkg ) {
601 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
605 'custnum' => $self->custnum,
606 'username' => $self->username,
607 'password' => $self->_password,
608 'first' => $cust_main->first,
609 'last' => $cust_main->getfield('last'),
610 'pkg' => $cust_pkg->part_pkg->pkg,
612 my $wqueue = new FS::queue {
613 'svcnum' => $self->svcnum,
614 'job' => 'FS::svc_acct::send_email'
616 my $error = $wqueue->insert(
618 'from' => $welcome_from,
619 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
620 'mimetype' => $welcome_mimetype,
621 'body' => $welcome_template->fill_in( HASH => \%hash, ),
624 $dbh->rollback if $oldAutoCommit;
625 return "error queuing welcome email: $error";
628 if ( $options{'depend_jobnum'} ) {
629 warn "$me depend_jobnum found; adding to welcome email dependancies"
631 if ( ref($options{'depend_jobnum'}) ) {
632 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
633 "to welcome email dependancies"
635 push @jobnums, @{ $options{'depend_jobnum'} };
637 warn "$me adding job $options{'depend_jobnum'} ".
638 "to welcome email dependancies"
640 push @jobnums, $options{'depend_jobnum'};
644 foreach my $jobnum ( @jobnums ) {
645 my $error = $wqueue->depend_insert($jobnum);
647 $dbh->rollback if $oldAutoCommit;
648 return "error queuing welcome email job dependancy: $error";
658 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
662 # set usage fields and thresholds if unset but set in a package def
663 sub preinsert_hook_first {
666 return '' unless $self->pkgnum;
668 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
669 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
670 return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
672 my %values = $part_pkg->usage_valuehash;
673 my $multiplier = $conf->exists('svc_acct-usage_threshold')
674 ? 1 - $conf->config('svc_acct-usage_threshold')/100
675 : 0.20; #doesn't matter
677 foreach ( keys %values ) {
678 next if $self->getfield($_);
679 $self->setfield( $_, $values{$_} );
680 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
681 if $conf->exists('svc_acct-usage_threshold');
689 Deletes this account from the database. If there is an error, returns the
690 error, otherwise returns false.
692 The corresponding FS::cust_svc record will be deleted as well.
694 (TODOC: new exports!)
701 return "can't delete system account" if $self->_check_system;
703 return "Can't delete an account which is a (svc_forward) source!"
704 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
706 return "Can't delete an account which is a (svc_forward) destination!"
707 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
709 return "Can't delete an account with (svc_www) web service!"
710 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
712 # what about records in session ? (they should refer to history table)
714 local $SIG{HUP} = 'IGNORE';
715 local $SIG{INT} = 'IGNORE';
716 local $SIG{QUIT} = 'IGNORE';
717 local $SIG{TERM} = 'IGNORE';
718 local $SIG{TSTP} = 'IGNORE';
719 local $SIG{PIPE} = 'IGNORE';
721 my $oldAutoCommit = $FS::UID::AutoCommit;
722 local $FS::UID::AutoCommit = 0;
725 foreach my $cust_main_invoice (
726 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
728 unless ( defined($cust_main_invoice) ) {
729 warn "WARNING: something's wrong with qsearch";
732 my %hash = $cust_main_invoice->hash;
733 $hash{'dest'} = $self->email;
734 my $new = new FS::cust_main_invoice \%hash;
735 my $error = $new->replace($cust_main_invoice);
737 $dbh->rollback if $oldAutoCommit;
742 foreach my $svc_domain (
743 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
745 my %hash = new FS::svc_domain->hash;
746 $hash{'catchall'} = '';
747 my $new = new FS::svc_domain \%hash;
748 my $error = $new->replace($svc_domain);
750 $dbh->rollback if $oldAutoCommit;
755 my $error = $self->SUPER::delete;
757 $dbh->rollback if $oldAutoCommit;
761 foreach my $radius_usergroup (
762 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
764 my $error = $radius_usergroup->delete;
766 $dbh->rollback if $oldAutoCommit;
771 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
775 =item replace OLD_RECORD
777 Replaces OLD_RECORD with this one in the database. If there is an error,
778 returns the error, otherwise returns false.
780 The additional field I<usergroup> can optionally be defined; if so it should
781 contain an arrayref of group names. See L<FS::radius_usergroup>.
789 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
793 warn "$me replacing $old with $new\n" if $DEBUG;
797 return "can't modify system account" if $old->_check_system;
800 #no warnings 'numeric'; #alas, a 5.006-ism
803 foreach my $xid (qw( uid gid )) {
805 return "Can't change $xid!"
806 if ! $conf->exists("svc_acct-edit_$xid")
807 && $old->$xid() != $new->$xid()
808 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
813 #change homdir when we change username
814 $new->setfield('dir', '') if $old->username ne $new->username;
816 local $SIG{HUP} = 'IGNORE';
817 local $SIG{INT} = 'IGNORE';
818 local $SIG{QUIT} = 'IGNORE';
819 local $SIG{TERM} = 'IGNORE';
820 local $SIG{TSTP} = 'IGNORE';
821 local $SIG{PIPE} = 'IGNORE';
823 my $oldAutoCommit = $FS::UID::AutoCommit;
824 local $FS::UID::AutoCommit = 0;
827 # redundant, but so $new->usergroup gets set
828 $error = $new->check;
829 return $error if $error;
831 $old->usergroup( [ $old->radius_groups ] );
833 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
834 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
836 if ( $new->usergroup ) {
837 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
838 my @newgroups = @{$new->usergroup};
839 foreach my $oldgroup ( @{$old->usergroup} ) {
840 if ( grep { $oldgroup eq $_ } @newgroups ) {
841 @newgroups = grep { $oldgroup ne $_ } @newgroups;
844 my $radius_usergroup = qsearchs('radius_usergroup', {
845 svcnum => $old->svcnum,
846 groupname => $oldgroup,
848 my $error = $radius_usergroup->delete;
850 $dbh->rollback if $oldAutoCommit;
851 return "error deleting radius_usergroup $oldgroup: $error";
855 foreach my $newgroup ( @newgroups ) {
856 my $radius_usergroup = new FS::radius_usergroup ( {
857 svcnum => $new->svcnum,
858 groupname => $newgroup,
860 my $error = $radius_usergroup->insert;
862 $dbh->rollback if $oldAutoCommit;
863 return "error adding radius_usergroup $newgroup: $error";
869 $error = $new->SUPER::replace($old, @_);
871 $dbh->rollback if $oldAutoCommit;
872 return $error if $error;
875 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
876 $error = $new->queue_fuzzyfiles_update;
878 $dbh->rollback if $oldAutoCommit;
879 return "updating fuzzy search cache: $error";
883 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
887 =item queue_fuzzyfiles_update
889 Used by insert & replace to update the fuzzy search cache
893 sub queue_fuzzyfiles_update {
896 local $SIG{HUP} = 'IGNORE';
897 local $SIG{INT} = 'IGNORE';
898 local $SIG{QUIT} = 'IGNORE';
899 local $SIG{TERM} = 'IGNORE';
900 local $SIG{TSTP} = 'IGNORE';
901 local $SIG{PIPE} = 'IGNORE';
903 my $oldAutoCommit = $FS::UID::AutoCommit;
904 local $FS::UID::AutoCommit = 0;
907 my $queue = new FS::queue {
908 'svcnum' => $self->svcnum,
909 'job' => 'FS::svc_acct::append_fuzzyfiles'
911 my $error = $queue->insert($self->username);
913 $dbh->rollback if $oldAutoCommit;
914 return "queueing job (transaction rolled back): $error";
917 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
925 Suspends this account by calling export-specific suspend hooks. If there is
926 an error, returns the error, otherwise returns false.
928 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
934 return "can't suspend system account" if $self->_check_system;
935 $self->SUPER::suspend(@_);
940 Unsuspends this account by by calling export-specific suspend hooks. If there
941 is an error, returns the error, otherwise returns false.
943 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
949 my %hash = $self->hash;
950 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
951 $hash{_password} = $1;
952 my $new = new FS::svc_acct ( \%hash );
953 my $error = $new->replace($self);
954 return $error if $error;
957 $self->SUPER::unsuspend(@_);
962 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
964 If the B<auto_unset_catchall> configuration option is set, this method will
965 automatically remove any references to the canceled service in the catchall
966 field of svc_domain. This allows packages that contain both a svc_domain and
967 its catchall svc_acct to be canceled in one step.
972 # Only one thing to do at this level
974 foreach my $svc_domain (
975 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
976 if($conf->exists('auto_unset_catchall')) {
977 my %hash = $svc_domain->hash;
978 $hash{catchall} = '';
979 my $new = new FS::svc_domain ( \%hash );
980 my $error = $new->replace($svc_domain);
981 return $error if $error;
983 return "cannot unprovision svc_acct #".$self->svcnum.
984 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
988 $self->SUPER::cancel(@_);
994 Checks all fields to make sure this is a valid service. If there is an error,
995 returns the error, otherwise returns false. Called by the insert and replace
998 Sets any fixed values; see L<FS::part_svc>.
1005 my($recref) = $self->hashref;
1007 my $x = $self->setfixed( $self->_fieldhandlers );
1008 return $x unless ref($x);
1011 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1013 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1016 my $error = $self->ut_numbern('svcnum')
1017 #|| $self->ut_number('domsvc')
1018 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1019 || $self->ut_textn('sec_phrase')
1020 || $self->ut_snumbern('seconds')
1021 || $self->ut_snumbern('upbytes')
1022 || $self->ut_snumbern('downbytes')
1023 || $self->ut_snumbern('totalbytes')
1024 || $self->ut_enum( '_password_encoding',
1025 [ '', qw( plain crypt ldap ) ]
1028 return $error if $error;
1031 local $username_letter = $username_letter;
1032 if ($self->svcnum) {
1033 my $cust_svc = $self->cust_svc
1034 or return "no cust_svc record found for svcnum ". $self->svcnum;
1035 my $cust_pkg = $cust_svc->cust_pkg;
1037 if ($self->pkgnum) {
1038 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );#complain?
1042 $conf->exists('username-letter', $cust_pkg->cust_main->agentnum);
1045 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1046 if ( $username_uppercase ) {
1047 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1048 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1049 $recref->{username} = $1;
1051 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1052 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1053 $recref->{username} = $1;
1056 if ( $username_letterfirst ) {
1057 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1058 } elsif ( $username_letter ) {
1059 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1061 if ( $username_noperiod ) {
1062 $recref->{username} =~ /\./ and return gettext('illegal_username');
1064 if ( $username_nounderscore ) {
1065 $recref->{username} =~ /_/ and return gettext('illegal_username');
1067 if ( $username_nodash ) {
1068 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1070 unless ( $username_ampersand ) {
1071 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1073 unless ( $username_percent ) {
1074 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1076 unless ( $username_colon ) {
1077 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1080 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1081 $recref->{popnum} = $1;
1082 return "Unknown popnum" unless
1083 ! $recref->{popnum} ||
1084 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1086 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1088 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1089 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1091 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1092 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1093 #not all systems use gid=uid
1094 #you can set a fixed gid in part_svc
1096 return "Only root can have uid 0"
1097 if $recref->{uid} == 0
1098 && $recref->{username} !~ /^(root|toor|smtp)$/;
1100 unless ( $recref->{username} eq 'sync' ) {
1101 if ( grep $_ eq $recref->{shell}, @shells ) {
1102 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1104 return "Illegal shell \`". $self->shell. "\'; ".
1105 "shells configuration value contains: @shells";
1108 $recref->{shell} = '/bin/sync';
1112 $recref->{gid} ne '' ?
1113 return "Can't have gid without uid" : ( $recref->{gid}='' );
1114 #$recref->{dir} ne '' ?
1115 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1116 $recref->{shell} ne '' ?
1117 return "Can't have shell without uid" : ( $recref->{shell}='' );
1120 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1122 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1123 or return "Illegal directory: ". $recref->{dir};
1124 $recref->{dir} = $1;
1125 return "Illegal directory"
1126 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1127 return "Illegal directory"
1128 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1129 unless ( $recref->{dir} ) {
1130 $recref->{dir} = $dir_prefix . '/';
1131 if ( $dirhash > 0 ) {
1132 for my $h ( 1 .. $dirhash ) {
1133 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1135 } elsif ( $dirhash < 0 ) {
1136 for my $h ( reverse $dirhash .. -1 ) {
1137 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1140 $recref->{dir} .= $recref->{username};
1146 # $error = $self->ut_textn('finger');
1147 # return $error if $error;
1148 if ( $self->getfield('finger') eq '' ) {
1149 my $cust_pkg = $self->svcnum
1150 ? $self->cust_svc->cust_pkg
1151 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1153 my $cust_main = $cust_pkg->cust_main;
1154 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1157 $self->getfield('finger') =~
1158 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1159 or return "Illegal finger: ". $self->getfield('finger');
1160 $self->setfield('finger', $1);
1162 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1163 $recref->{quota} = $1;
1165 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1166 if ( $recref->{slipip} eq '' ) {
1167 $recref->{slipip} = '';
1168 } elsif ( $recref->{slipip} eq '0e0' ) {
1169 $recref->{slipip} = '0e0';
1171 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1172 or return "Illegal slipip: ". $self->slipip;
1173 $recref->{slipip} = $1;
1178 #arbitrary RADIUS stuff; allow ut_textn for now
1179 foreach ( grep /^radius_/, fields('svc_acct') ) {
1180 $self->ut_textn($_);
1183 if ( $recref->{_password_encoding} eq 'ldap' ) {
1185 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1186 $recref->{_password} = uc($1).$2;
1188 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1191 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1193 if ( $recref->{_password} =~
1194 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1195 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1198 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1201 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1204 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1206 #generate a password if it is blank
1207 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1208 unless length( $recref->{_password} );
1210 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1211 $recref->{_password} = $1;
1213 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1214 FS::Msgcat::_gettext('illegal_password_characters').
1215 ": ". $recref->{_password};
1218 if ( $password_noampersand ) {
1219 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1221 if ( $password_noexclamation ) {
1222 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1227 #carp "warning: _password_encoding unspecified\n";
1229 #generate a password if it is blank
1230 unless ( length($recref->{_password}) || ! $passwordmin ) {
1232 $recref->{_password} =
1233 join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1234 $recref->{_password_encoding} = 'plain';
1238 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1239 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1240 $recref->{_password} = $1.$3;
1241 $recref->{_password_encoding} = 'plain';
1242 } elsif ( $recref->{_password} =~
1243 /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/
1245 $recref->{_password} = $1.$3;
1246 $recref->{_password_encoding} = 'crypt';
1247 } elsif ( $recref->{_password} eq '*' ) {
1248 $recref->{_password} = '*';
1249 $recref->{_password_encoding} = 'crypt';
1250 } elsif ( $recref->{_password} eq '!' ) {
1251 $recref->{_password_encoding} = 'crypt';
1252 $recref->{_password} = '!';
1253 } elsif ( $recref->{_password} eq '!!' ) {
1254 $recref->{_password} = '!!';
1255 $recref->{_password_encoding} = 'crypt';
1257 #return "Illegal password";
1258 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1259 FS::Msgcat::_gettext('illegal_password_characters').
1260 ": ". $recref->{_password};
1267 $self->SUPER::check;
1273 Internal function to check the username against the list of system usernames
1274 from the I<system_usernames> configuration value. Returns true if the username
1275 is listed on the system username list.
1281 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1282 $conf->config('system_usernames')
1286 =item _check_duplicate
1288 Internal method to check for duplicates usernames, username@domain pairs and
1291 If the I<global_unique-username> configuration value is set to B<username> or
1292 B<username@domain>, enforces global username or username@domain uniqueness.
1294 In all cases, check for duplicate uids and usernames or username@domain pairs
1295 per export and with identical I<svcpart> values.
1299 sub _check_duplicate {
1302 my $global_unique = $conf->config('global_unique-username') || 'none';
1303 return '' if $global_unique eq 'disabled';
1307 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1308 unless ( $part_svc ) {
1309 return 'unknown svcpart '. $self->svcpart;
1312 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1313 qsearch( 'svc_acct', { 'username' => $self->username } );
1314 return gettext('username_in_use')
1315 if $global_unique eq 'username' && @dup_user;
1317 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1318 qsearch( 'svc_acct', { 'username' => $self->username,
1319 'domsvc' => $self->domsvc } );
1320 return gettext('username_in_use')
1321 if $global_unique eq 'username@domain' && @dup_userdomain;
1324 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1325 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1326 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1327 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1332 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1333 my $exports = FS::part_export::export_info('svc_acct');
1334 my %conflict_user_svcpart;
1335 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1337 foreach my $part_export ( $part_svc->part_export ) {
1339 #this will catch to the same exact export
1340 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1342 #this will catch to exports w/same exporthost+type ???
1343 #my @other_part_export = qsearch('part_export', {
1344 # 'machine' => $part_export->machine,
1345 # 'exporttype' => $part_export->exporttype,
1347 #foreach my $other_part_export ( @other_part_export ) {
1348 # push @svcparts, map { $_->svcpart }
1349 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1352 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1353 #silly kludge to avoid uninitialized value errors
1354 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1355 ? $exports->{$part_export->exporttype}{'nodomain'}
1357 if ( $nodomain =~ /^Y/i ) {
1358 $conflict_user_svcpart{$_} = $part_export->exportnum
1361 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1366 foreach my $dup_user ( @dup_user ) {
1367 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1368 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1369 return "duplicate username ". $self->username.
1370 ": conflicts with svcnum ". $dup_user->svcnum.
1371 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1375 foreach my $dup_userdomain ( @dup_userdomain ) {
1376 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1377 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1378 return "duplicate username\@domain ". $self->email.
1379 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1380 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1384 foreach my $dup_uid ( @dup_uid ) {
1385 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1386 if ( exists($conflict_user_svcpart{$dup_svcpart})
1387 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1388 return "duplicate uid ". $self->uid.
1389 ": conflicts with svcnum ". $dup_uid->svcnum.
1391 ( $conflict_user_svcpart{$dup_svcpart}
1392 || $conflict_userdomain_svcpart{$dup_svcpart} );
1404 Depriciated, use radius_reply instead.
1409 carp "FS::svc_acct::radius depriciated, use radius_reply";
1410 $_[0]->radius_reply;
1415 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1416 reply attributes of this record.
1418 Note that this is now the preferred method for reading RADIUS attributes -
1419 accessing the columns directly is discouraged, as the column names are
1420 expected to change in the future.
1427 return %{ $self->{'radius_reply'} }
1428 if exists $self->{'radius_reply'};
1433 my($column, $attrib) = ($1, $2);
1434 #$attrib =~ s/_/\-/g;
1435 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1436 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1438 if ( $self->slipip && $self->slipip ne '0e0' ) {
1439 $reply{$radius_ip} = $self->slipip;
1442 if ( $self->seconds !~ /^$/ ) {
1443 $reply{'Session-Timeout'} = $self->seconds;
1446 if ( $conf->exists('radius-chillispot-max') ) {
1447 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1449 #hmm. just because sqlradius.pm says so?
1456 foreach my $what (qw( input output total )) {
1457 my $is = $whatis{$what}.'bytes';
1458 if ( $self->$is() =~ /\d/ ) {
1459 my $big = new Math::BigInt $self->$is();
1460 $big = new Math::BigInt '0' if $big->is_neg();
1461 my $att = "Chillispot-Max-\u$what";
1462 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1463 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1474 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1475 check attributes of this record.
1477 Note that this is now the preferred method for reading RADIUS attributes -
1478 accessing the columns directly is discouraged, as the column names are
1479 expected to change in the future.
1486 return %{ $self->{'radius_check'} }
1487 if exists $self->{'radius_check'};
1492 my($column, $attrib) = ($1, $2);
1493 #$attrib =~ s/_/\-/g;
1494 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1495 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1498 my($pw_attrib, $password) = $self->radius_password;
1499 $check{$pw_attrib} = $password;
1501 my $cust_svc = $self->cust_svc;
1503 my $cust_pkg = $cust_svc->cust_pkg;
1504 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1505 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1508 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1509 "; can't set Expiration\n"
1517 =item radius_password
1519 Returns a key/value pair containing the RADIUS attribute name and value
1524 sub radius_password {
1527 my($pw_attrib, $password);
1528 if ( $self->_password_encoding eq 'ldap' ) {
1530 $pw_attrib = 'Password-With-Header';
1531 $password = $self->_password;
1533 } elsif ( $self->_password_encoding eq 'crypt' ) {
1535 $pw_attrib = 'Crypt-Password';
1536 $password = $self->_password;
1538 } elsif ( $self->_password_encoding eq 'plain' ) {
1540 $pw_attrib = $radius_password; #Cleartext-Password? man rlm_pap
1541 $password = $self->_password;
1545 $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
1546 $password = $self->_password;
1550 ($pw_attrib, $password);
1556 This method instructs the object to "snapshot" or freeze RADIUS check and
1557 reply attributes to the current values.
1561 #bah, my english is too broken this morning
1562 #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
1563 #the FS::cust_pkg's replace method to trigger the correct export updates when
1564 #package dates change)
1569 $self->{$_} = { $self->$_() }
1570 foreach qw( radius_reply radius_check );
1574 =item forget_snapshot
1576 This methos instructs the object to forget any previously snapshotted
1577 RADIUS check and reply attributes.
1581 sub forget_snapshot {
1585 foreach qw( radius_reply radius_check );
1589 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1591 Returns the domain associated with this account.
1593 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1600 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1601 my $svc_domain = $self->svc_domain(@_)
1602 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1603 $svc_domain->domain;
1608 Returns the FS::svc_domain record for this account's domain (see
1613 # FS::h_svc_acct has a history-aware svc_domain override
1618 ? $self->{'_domsvc'}
1619 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1624 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1628 #inherited from svc_Common
1630 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1632 Returns an email address associated with the account.
1634 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1641 $self->username. '@'. $self->domain(@_);
1646 Returns an array of FS::acct_snarf records associated with the account.
1647 If the acct_snarf table does not exist or there are no associated records,
1648 an empty list is returned
1654 return () unless dbdef->table('acct_snarf');
1655 eval "use FS::acct_snarf;";
1657 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1660 =item decrement_upbytes OCTETS
1662 Decrements the I<upbytes> field of this record by the given amount. If there
1663 is an error, returns the error, otherwise returns false.
1667 sub decrement_upbytes {
1668 shift->_op_usage('-', 'upbytes', @_);
1671 =item increment_upbytes OCTETS
1673 Increments the I<upbytes> field of this record by the given amount. If there
1674 is an error, returns the error, otherwise returns false.
1678 sub increment_upbytes {
1679 shift->_op_usage('+', 'upbytes', @_);
1682 =item decrement_downbytes OCTETS
1684 Decrements the I<downbytes> field of this record by the given amount. If there
1685 is an error, returns the error, otherwise returns false.
1689 sub decrement_downbytes {
1690 shift->_op_usage('-', 'downbytes', @_);
1693 =item increment_downbytes OCTETS
1695 Increments the I<downbytes> field of this record by the given amount. If there
1696 is an error, returns the error, otherwise returns false.
1700 sub increment_downbytes {
1701 shift->_op_usage('+', 'downbytes', @_);
1704 =item decrement_totalbytes OCTETS
1706 Decrements the I<totalbytes> field of this record by the given amount. If there
1707 is an error, returns the error, otherwise returns false.
1711 sub decrement_totalbytes {
1712 shift->_op_usage('-', 'totalbytes', @_);
1715 =item increment_totalbytes OCTETS
1717 Increments the I<totalbytes> field of this record by the given amount. If there
1718 is an error, returns the error, otherwise returns false.
1722 sub increment_totalbytes {
1723 shift->_op_usage('+', 'totalbytes', @_);
1726 =item decrement_seconds SECONDS
1728 Decrements the I<seconds> field of this record by the given amount. If there
1729 is an error, returns the error, otherwise returns false.
1733 sub decrement_seconds {
1734 shift->_op_usage('-', 'seconds', @_);
1737 =item increment_seconds SECONDS
1739 Increments the I<seconds> field of this record by the given amount. If there
1740 is an error, returns the error, otherwise returns false.
1744 sub increment_seconds {
1745 shift->_op_usage('+', 'seconds', @_);
1753 my %op2condition = (
1754 '-' => sub { my($self, $column, $amount) = @_;
1755 $self->$column - $amount <= 0;
1757 '+' => sub { my($self, $column, $amount) = @_;
1758 ($self->$column || 0) + $amount > 0;
1761 my %op2warncondition = (
1762 '-' => sub { my($self, $column, $amount) = @_;
1763 my $threshold = $column . '_threshold';
1764 $self->$column - $amount <= $self->$threshold + 0;
1766 '+' => sub { my($self, $column, $amount) = @_;
1767 ($self->$column || 0) + $amount > 0;
1772 my( $self, $op, $column, $amount ) = @_;
1774 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1775 ' ('. $self->email. "): $op $amount\n"
1778 return '' unless $amount;
1780 local $SIG{HUP} = 'IGNORE';
1781 local $SIG{INT} = 'IGNORE';
1782 local $SIG{QUIT} = 'IGNORE';
1783 local $SIG{TERM} = 'IGNORE';
1784 local $SIG{TSTP} = 'IGNORE';
1785 local $SIG{PIPE} = 'IGNORE';
1787 my $oldAutoCommit = $FS::UID::AutoCommit;
1788 local $FS::UID::AutoCommit = 0;
1791 my $sql = "UPDATE svc_acct SET $column = ".
1792 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1793 " $op ? WHERE svcnum = ?";
1797 my $sth = $dbh->prepare( $sql )
1798 or die "Error preparing $sql: ". $dbh->errstr;
1799 my $rv = $sth->execute($amount, $self->svcnum);
1800 die "Error executing $sql: ". $sth->errstr
1801 unless defined($rv);
1802 die "Can't update $column for svcnum". $self->svcnum
1805 #$self->snapshot; #not necessary, we retain the old values
1806 #create an object with the updated usage values
1807 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1809 my $error = $new->replace($self);
1811 $dbh->rollback if $oldAutoCommit;
1812 return "Error replacing: $error";
1815 #overlimit_action eq 'cancel' handling
1816 my $cust_pkg = $self->cust_svc->cust_pkg;
1818 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
1819 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
1823 my $error = $cust_pkg->cancel; #XXX should have a reason
1825 $dbh->rollback if $oldAutoCommit;
1826 return "Error cancelling: $error";
1829 #nothing else is relevant if we're cancelling, so commit & return success
1830 warn "$me update successful; committing\n"
1832 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1837 my $action = $op2action{$op};
1839 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1840 ( $action eq 'suspend' && !$self->overlimit
1841 || $action eq 'unsuspend' && $self->overlimit )
1843 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1844 if ($part_export->option('overlimit_groups')) {
1846 my $other = new FS::svc_acct $self->hashref;
1847 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1848 ($self, $part_export->option('overlimit_groups'));
1849 $other->usergroup( $groups );
1850 if ($action eq 'suspend'){
1851 $new = $other; $old = $self;
1853 $new = $self; $old = $other;
1855 my $error = $part_export->export_replace($new, $old);
1856 $error ||= $self->overlimit($action);
1858 $dbh->rollback if $oldAutoCommit;
1859 return "Error replacing radius groups in export, ${op}: $error";
1865 if ( $conf->exists("svc_acct-usage_$action")
1866 && &{$op2condition{$op}}($self, $column, $amount) ) {
1867 #my $error = $self->$action();
1868 my $error = $self->cust_svc->cust_pkg->$action();
1869 # $error ||= $self->overlimit($action);
1871 $dbh->rollback if $oldAutoCommit;
1872 return "Error ${action}ing: $error";
1876 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1877 my $wqueue = new FS::queue {
1878 'svcnum' => $self->svcnum,
1879 'job' => 'FS::svc_acct::reached_threshold',
1884 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1888 my $error = $wqueue->insert(
1889 'svcnum' => $self->svcnum,
1891 'column' => $column,
1895 $dbh->rollback if $oldAutoCommit;
1896 return "Error queuing threshold activity: $error";
1900 warn "$me update successful; committing\n"
1902 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1908 my( $self, $valueref, %options ) = @_;
1910 warn "$me set_usage called for svcnum ". $self->svcnum.
1911 ' ('. $self->email. "): ".
1912 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1915 local $SIG{HUP} = 'IGNORE';
1916 local $SIG{INT} = 'IGNORE';
1917 local $SIG{QUIT} = 'IGNORE';
1918 local $SIG{TERM} = 'IGNORE';
1919 local $SIG{TSTP} = 'IGNORE';
1920 local $SIG{PIPE} = 'IGNORE';
1922 local $FS::svc_Common::noexport_hack = 1;
1923 my $oldAutoCommit = $FS::UID::AutoCommit;
1924 local $FS::UID::AutoCommit = 0;
1929 if ( $options{null} ) {
1930 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1931 qw( seconds upbytes downbytes totalbytes )
1934 foreach my $field (keys %$valueref){
1935 $reset = 1 if $valueref->{$field};
1936 $self->setfield($field, $valueref->{$field});
1937 $self->setfield( $field.'_threshold',
1938 int($self->getfield($field)
1939 * ( $conf->exists('svc_acct-usage_threshold')
1940 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1945 $handyhash{$field} = $self->getfield($field);
1946 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1948 #my $error = $self->replace; #NO! we avoid the call to ->check for
1949 #die $error if $error; #services not explicity changed via the UI
1951 my $sql = "UPDATE svc_acct SET " .
1952 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1953 " WHERE svcnum = ". $self->svcnum;
1958 if (scalar(keys %handyhash)) {
1959 my $sth = $dbh->prepare( $sql )
1960 or die "Error preparing $sql: ". $dbh->errstr;
1961 my $rv = $sth->execute();
1962 die "Error executing $sql: ". $sth->errstr
1963 unless defined($rv);
1964 die "Can't update usage for svcnum ". $self->svcnum
1968 #$self->snapshot; #not necessary, we retain the old values
1969 #create an object with the updated usage values
1970 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1972 my $error = $new->replace($self);
1974 $dbh->rollback if $oldAutoCommit;
1975 return "Error replacing: $error";
1981 if ($self->overlimit) {
1982 $error = $self->overlimit('unsuspend');
1983 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1984 if ($part_export->option('overlimit_groups')) {
1985 my $old = new FS::svc_acct $self->hashref;
1986 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1987 ($self, $part_export->option('overlimit_groups'));
1988 $old->usergroup( $groups );
1989 $error ||= $part_export->export_replace($self, $old);
1994 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1995 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1998 $dbh->rollback if $oldAutoCommit;
1999 return "Error unsuspending: $error";
2003 warn "$me update successful; committing\n"
2005 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2011 =item recharge HASHREF
2013 Increments usage columns by the amount specified in HASHREF as
2014 column=>amount pairs.
2019 my ($self, $vhash) = @_;
2022 warn "[$me] recharge called on $self: ". Dumper($self).
2023 "\nwith vhash: ". Dumper($vhash);
2026 my $oldAutoCommit = $FS::UID::AutoCommit;
2027 local $FS::UID::AutoCommit = 0;
2031 foreach my $column (keys %$vhash){
2032 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
2036 $dbh->rollback if $oldAutoCommit;
2038 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2043 =item is_rechargeable
2045 Returns true if this svc_account can be "recharged" and false otherwise.
2049 sub is_rechargable {
2051 $self->seconds ne ''
2052 || $self->upbytes ne ''
2053 || $self->downbytes ne ''
2054 || $self->totalbytes ne '';
2057 =item seconds_since TIMESTAMP
2059 Returns the number of seconds this account has been online since TIMESTAMP,
2060 according to the session monitor (see L<FS::Session>).
2062 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2063 L<Time::Local> and L<Date::Parse> for conversion functions.
2067 #note: POD here, implementation in FS::cust_svc
2070 $self->cust_svc->seconds_since(@_);
2073 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
2075 Returns the numbers of seconds this account has been online between
2076 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
2077 external SQL radacct table, specified via sqlradius export. Sessions which
2078 started in the specified range but are still open are counted from session
2079 start to the end of the range (unless they are over 1 day old, in which case
2080 they are presumed missing their stop record and not counted). Also, sessions
2081 which end in the range but started earlier are counted from the start of the
2082 range to session end. Finally, sessions which start before the range but end
2083 after are counted for the entire range.
2085 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2086 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2091 #note: POD here, implementation in FS::cust_svc
2092 sub seconds_since_sqlradacct {
2094 $self->cust_svc->seconds_since_sqlradacct(@_);
2097 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2099 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2100 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2101 TIMESTAMP_END (exclusive).
2103 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2104 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2109 #note: POD here, implementation in FS::cust_svc
2110 sub attribute_since_sqlradacct {
2112 $self->cust_svc->attribute_since_sqlradacct(@_);
2115 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2117 Returns an array of hash references of this customers login history for the
2118 given time range. (document this better)
2122 sub get_session_history {
2124 $self->cust_svc->get_session_history(@_);
2127 =item last_login_text
2129 Returns text describing the time of last login.
2133 sub last_login_text {
2135 $self->last_login ? ctime($self->last_login) : 'unknown';
2138 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2143 my($self, $start, $end, %opt ) = @_;
2145 my $did = $self->username; #yup
2147 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2149 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2151 #SELECT $for_update * FROM cdr
2152 # WHERE calldate >= $start #need a conversion
2153 # AND calldate < $end #ditto
2154 # AND ( charged_party = "$did"
2155 # OR charged_party = "$prefix$did" #if length($prefix);
2156 # OR ( ( charged_party IS NULL OR charged_party = '' )
2158 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2161 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2164 if ( length($prefix) ) {
2166 " AND ( charged_party = '$did'
2167 OR charged_party = '$prefix$did'
2168 OR ( ( charged_party IS NULL OR charged_party = '' )
2170 ( src = '$did' OR src = '$prefix$did' )
2176 " AND ( charged_party = '$did'
2177 OR ( ( charged_party IS NULL OR charged_party = '' )
2187 'select' => "$for_update *",
2190 #( freesidestatus IS NULL OR freesidestatus = '' )
2191 'freesidestatus' => '',
2193 'extra_sql' => $charged_or_src,
2201 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2207 if ( $self->usergroup ) {
2208 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2209 unless ref($self->usergroup) eq 'ARRAY';
2210 #when provisioning records, export callback runs in svc_Common.pm before
2211 #radius_usergroup records can be inserted...
2212 @{$self->usergroup};
2214 map { $_->groupname }
2215 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2219 =item clone_suspended
2221 Constructor used by FS::part_export::_export_suspend fallback. Document
2226 sub clone_suspended {
2228 my %hash = $self->hash;
2229 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2230 new FS::svc_acct \%hash;
2233 =item clone_kludge_unsuspend
2235 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2240 sub clone_kludge_unsuspend {
2242 my %hash = $self->hash;
2243 $hash{_password} = '';
2244 new FS::svc_acct \%hash;
2247 =item check_password
2249 Checks the supplied password against the (possibly encrypted) password in the
2250 database. Returns true for a successful authentication, false for no match.
2252 Currently supported encryptions are: classic DES crypt() and MD5
2256 sub check_password {
2257 my($self, $check_password) = @_;
2259 #remove old-style SUSPENDED kludge, they should be allowed to login to
2260 #self-service and pay up
2261 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2263 if ( $self->_password_encoding eq 'ldap' ) {
2265 my $auth = from_rfc2307 Authen::Passphrase $self->_password;
2266 return $auth->match($check_password);
2268 } elsif ( $self->_password_encoding eq 'crypt' ) {
2270 my $auth = from_crypt Authen::Passphrase $self->_password;
2271 return $auth->match($check_password);
2273 } elsif ( $self->_password_encoding eq 'plain' ) {
2275 return $check_password eq $password;
2279 #XXX this could be replaced with Authen::Passphrase stuff
2281 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2283 } elsif ( length($password) < 13 ) { #plaintext
2284 $check_password eq $password;
2285 } elsif ( length($password) == 13 ) { #traditional DES crypt
2286 crypt($check_password, $password) eq $password;
2287 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2288 unix_md5_crypt($check_password, $password) eq $password;
2289 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2290 warn "Can't check password: Blowfish encryption not yet supported, ".
2291 "svcnum ". $self->svcnum. "\n";
2294 warn "Can't check password: Unrecognized encryption for svcnum ".
2295 $self->svcnum. "\n";
2303 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2305 Returns an encrypted password, either by passing through an encrypted password
2306 in the database or by encrypting a plaintext password from the database.
2308 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2309 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2310 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2311 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2312 encryption type is only used if the password is not already encrypted in the
2317 sub crypt_password {
2320 if ( $self->_password_encoding eq 'ldap' ) {
2322 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2325 #XXX this could be replaced with Authen::Passphrase stuff
2327 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2328 if ( $encryption eq 'crypt' ) {
2331 $saltset[int(rand(64))].$saltset[int(rand(64))]
2333 } elsif ( $encryption eq 'md5' ) {
2334 unix_md5_crypt( $self->_password );
2335 } elsif ( $encryption eq 'blowfish' ) {
2336 croak "unknown encryption method $encryption";
2338 croak "unknown encryption method $encryption";
2341 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2345 } elsif ( $self->_password_encoding eq 'crypt' ) {
2347 return $self->_password;
2349 } elsif ( $self->_password_encoding eq 'plain' ) {
2351 #XXX this could be replaced with Authen::Passphrase stuff
2353 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2354 if ( $encryption eq 'crypt' ) {
2357 $saltset[int(rand(64))].$saltset[int(rand(64))]
2359 } elsif ( $encryption eq 'md5' ) {
2360 unix_md5_crypt( $self->_password );
2361 } elsif ( $encryption eq 'blowfish' ) {
2362 croak "unknown encryption method $encryption";
2364 croak "unknown encryption method $encryption";
2369 if ( length($self->_password) == 13
2370 || $self->_password =~ /^\$(1|2a?)\$/
2371 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2377 #XXX this could be replaced with Authen::Passphrase stuff
2379 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2380 if ( $encryption eq 'crypt' ) {
2383 $saltset[int(rand(64))].$saltset[int(rand(64))]
2385 } elsif ( $encryption eq 'md5' ) {
2386 unix_md5_crypt( $self->_password );
2387 } elsif ( $encryption eq 'blowfish' ) {
2388 croak "unknown encryption method $encryption";
2390 croak "unknown encryption method $encryption";
2399 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2401 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2402 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2403 "{MD5}5426824942db4253f87a1009fd5d2d4".
2405 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2406 to work the same as the B</crypt_password> method.
2412 #eventually should check a "password-encoding" field
2414 if ( $self->_password_encoding eq 'ldap' ) {
2416 return $self->_password;
2418 } elsif ( $self->_password_encoding eq 'crypt' ) {
2420 if ( length($self->_password) == 13 ) { #crypt
2421 return '{CRYPT}'. $self->_password;
2422 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2424 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2425 # die "Blowfish encryption not supported in this context, svcnum ".
2426 # $self->svcnum. "\n";
2428 warn "encryption method not (yet?) supported in LDAP context";
2429 return '{CRYPT}*'; #unsupported, should not auth
2432 } elsif ( $self->_password_encoding eq 'plain' ) {
2434 return '{PLAIN}'. $self->_password;
2436 #return '{CLEARTEXT}'. $self->_password; #?
2440 if ( length($self->_password) == 13 ) { #crypt
2441 return '{CRYPT}'. $self->_password;
2442 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2444 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2445 warn "Blowfish encryption not supported in this context, svcnum ".
2446 $self->svcnum. "\n";
2449 #are these two necessary anymore?
2450 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2451 return '{SSHA}'. $1;
2452 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2453 return '{NS-MTA-MD5}'. $1;
2456 return '{PLAIN}'. $self->_password;
2458 #return '{CLEARTEXT}'. $self->_password; #?
2460 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2461 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2462 #if ( $encryption eq 'crypt' ) {
2463 # return '{CRYPT}'. crypt(
2465 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2467 #} elsif ( $encryption eq 'md5' ) {
2468 # unix_md5_crypt( $self->_password );
2469 #} elsif ( $encryption eq 'blowfish' ) {
2470 # croak "unknown encryption method $encryption";
2472 # croak "unknown encryption method $encryption";
2480 =item domain_slash_username
2482 Returns $domain/$username/
2486 sub domain_slash_username {
2488 $self->domain. '/'. $self->username. '/';
2491 =item virtual_maildir
2493 Returns $domain/maildirs/$username/
2497 sub virtual_maildir {
2499 $self->domain. '/maildirs/'. $self->username. '/';
2504 =head1 CLASS METHODS
2508 =item search HASHREF
2510 Class method which returns a qsearch hash expression to search for parameters
2511 specified in HASHREF. Valid parameters are
2525 Arrayref of pkgparts
2531 Arrayref of additional WHERE clauses, will be ANDed together.
2542 my ($class, $params) = @_;
2547 if ( $params->{'domain'} ) {
2548 my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
2549 #preserve previous behavior & bubble up an error if $svc_domain not found?
2550 push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
2554 if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
2555 push @where, "domsvc = $1";
2559 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
2562 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
2563 push @where, "agentnum = $1";
2567 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
2568 push @where, "custnum = $1";
2572 if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
2573 #XXX untaint or sql quote
2575 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
2579 if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
2580 push @where, "popnum = $1";
2584 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
2585 push @where, "svcpart = $1";
2589 # here is the agent virtualization
2590 #if ($params->{CurrentUser}) {
2592 # qsearchs('access_user', { username => $params->{CurrentUser} });
2594 # if ($access_user) {
2595 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
2597 # push @where, "1=0";
2600 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
2601 'table' => 'cust_main',
2602 'null_right' => 'View/link unlinked services',
2606 push @where, @{ $params->{'where'} } if $params->{'where'};
2608 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
2610 my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
2611 ' LEFT JOIN part_svc USING ( svcpart ) '.
2612 ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
2613 ' LEFT JOIN cust_main USING ( custnum ) ';
2615 my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
2616 #if ( keys %svc_acct ) {
2617 # $count_query .= ' WHERE '.
2618 # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
2624 'table' => 'svc_acct',
2625 'hashref' => {}, # \%svc_acct,
2626 'select' => join(', ',
2629 'cust_main.custnum',
2630 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
2632 'addl_from' => $addl_from,
2633 'extra_sql' => $extra_sql,
2634 'order_by' => $params->{'order_by'},
2635 'count_query' => $count_query,
2648 This is the FS::svc_acct job-queue-able version. It still uses
2649 FS::Misc::send_email under-the-hood.
2656 eval "use FS::Misc qw(send_email)";
2659 $opt{mimetype} ||= 'text/plain';
2660 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2662 my $error = send_email(
2663 'from' => $opt{from},
2665 'subject' => $opt{subject},
2666 'content-type' => $opt{mimetype},
2667 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2669 die $error if $error;
2672 =item check_and_rebuild_fuzzyfiles
2676 sub check_and_rebuild_fuzzyfiles {
2677 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2678 -e "$dir/svc_acct.username"
2679 or &rebuild_fuzzyfiles;
2682 =item rebuild_fuzzyfiles
2686 sub rebuild_fuzzyfiles {
2688 use Fcntl qw(:flock);
2690 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2694 open(USERNAMELOCK,">>$dir/svc_acct.username")
2695 or die "can't open $dir/svc_acct.username: $!";
2696 flock(USERNAMELOCK,LOCK_EX)
2697 or die "can't lock $dir/svc_acct.username: $!";
2699 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2701 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2702 or die "can't open $dir/svc_acct.username.tmp: $!";
2703 print USERNAMECACHE join("\n", @all_username), "\n";
2704 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2706 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2716 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2717 open(USERNAMECACHE,"<$dir/svc_acct.username")
2718 or die "can't open $dir/svc_acct.username: $!";
2719 my @array = map { chomp; $_; } <USERNAMECACHE>;
2720 close USERNAMECACHE;
2724 =item append_fuzzyfiles USERNAME
2728 sub append_fuzzyfiles {
2729 my $username = shift;
2731 &check_and_rebuild_fuzzyfiles;
2733 use Fcntl qw(:flock);
2735 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2737 open(USERNAME,">>$dir/svc_acct.username")
2738 or die "can't open $dir/svc_acct.username: $!";
2739 flock(USERNAME,LOCK_EX)
2740 or die "can't lock $dir/svc_acct.username: $!";
2742 print USERNAME "$username\n";
2744 flock(USERNAME,LOCK_UN)
2745 or die "can't unlock $dir/svc_acct.username: $!";
2753 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2757 sub radius_usergroup_selector {
2758 my $sel_groups = shift;
2759 my %sel_groups = map { $_=>1 } @$sel_groups;
2761 my $selectname = shift || 'radius_usergroup';
2764 my $sth = $dbh->prepare(
2765 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2766 ) or die $dbh->errstr;
2767 $sth->execute() or die $sth->errstr;
2768 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2772 function ${selectname}_doadd(object) {
2773 var myvalue = object.${selectname}_add.value;
2774 var optionName = new Option(myvalue,myvalue,false,true);
2775 var length = object.$selectname.length;
2776 object.$selectname.options[length] = optionName;
2777 object.${selectname}_add.value = "";
2780 <SELECT MULTIPLE NAME="$selectname">
2783 foreach my $group ( @all_groups ) {
2784 $html .= qq(<OPTION VALUE="$group");
2785 if ( $sel_groups{$group} ) {
2786 $html .= ' SELECTED';
2787 $sel_groups{$group} = 0;
2789 $html .= ">$group</OPTION>\n";
2791 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2792 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2794 $html .= '</SELECT>';
2796 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2797 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2802 =item reached_threshold
2804 Performs some activities when svc_acct thresholds (such as number of seconds
2805 remaining) are reached.
2809 sub reached_threshold {
2812 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2813 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2815 if ( $opt{'op'} eq '+' ){
2816 $svc_acct->setfield( $opt{'column'}.'_threshold',
2817 int($svc_acct->getfield($opt{'column'})
2818 * ( $conf->exists('svc_acct-usage_threshold')
2819 ? $conf->config('svc_acct-usage_threshold')/100
2824 my $error = $svc_acct->replace;
2825 die $error if $error;
2826 }elsif ( $opt{'op'} eq '-' ){
2828 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2829 return '' if ($threshold eq '' );
2831 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2832 my $error = $svc_acct->replace;
2833 die $error if $error; # email next time, i guess
2835 if ( $warning_template ) {
2836 eval "use FS::Misc qw(send_email)";
2839 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2840 my $cust_main = $cust_pkg->cust_main;
2842 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2843 $cust_main->invoicing_list,
2844 ($opt{'to'} ? $opt{'to'} : ())
2847 my $mimetype = $warning_mimetype;
2848 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2850 my $body = $warning_template->fill_in( HASH => {
2851 'custnum' => $cust_main->custnum,
2852 'username' => $svc_acct->username,
2853 'password' => $svc_acct->_password,
2854 'first' => $cust_main->first,
2855 'last' => $cust_main->getfield('last'),
2856 'pkg' => $cust_pkg->part_pkg->pkg,
2857 'column' => $opt{'column'},
2858 'amount' => $opt{'column'} =~/bytes/
2859 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2860 : $svc_acct->getfield($opt{'column'}),
2861 'threshold' => $opt{'column'} =~/bytes/
2862 ? FS::UI::bytecount::display_bytecount($threshold)
2867 my $error = send_email(
2868 'from' => $warning_from,
2870 'subject' => $warning_subject,
2871 'content-type' => $mimetype,
2872 'body' => [ map "$_\n", split("\n", $body) ],
2874 die $error if $error;
2877 die "unknown op: " . $opt{'op'};
2885 The $recref stuff in sub check should be cleaned up.
2887 The suspend, unsuspend and cancel methods update the database, but not the
2888 current object. This is probably a bug as it's unexpected and
2891 radius_usergroup_selector? putting web ui components in here? they should
2892 probably live somewhere else...
2894 insertion of RADIUS group stuff in insert could be done with child_objects now
2895 (would probably clean up export of them too)
2897 _op_usage and set_usage bypass the history... maybe they shouldn't
2901 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2902 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2903 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2904 L<freeside-queued>), L<FS::svc_acct_pop>,
2905 schema.html from the base documentation.
2909 =item domain_select_hash %OPTIONS
2911 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2912 may at present purchase.
2914 Currently available options are: I<pkgnum> I<svcpart>
2918 sub domain_select_hash {
2919 my ($self, %options) = @_;
2925 $part_svc = $self->part_svc;
2926 $cust_pkg = $self->cust_svc->cust_pkg
2930 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2931 if $options{'svcpart'};
2933 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2934 if $options{'pkgnum'};
2936 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2937 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2938 %domains = map { $_->svcnum => $_->domain }
2939 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2940 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2941 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2942 %domains = map { $_->svcnum => $_->domain }
2943 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2944 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2945 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2947 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2950 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2951 my $svc_domain = qsearchs('svc_domain',
2952 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2953 if ( $svc_domain ) {
2954 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2956 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2957 $part_svc->part_svc_column('domsvc')->columnvalue;