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
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 );
21 use Crypt::PasswdMD5 1.2;
24 use Authen::Passphrase;
25 use FS::UID qw( datasrc driver_name );
27 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
28 use FS::Msgcat qw(gettext);
29 use FS::UI::bytecount;
35 use FS::cust_main_invoice;
39 use FS::radius_usergroup;
46 @ISA = qw( FS::svc_Common );
49 $me = '[FS::svc_acct]';
51 #ask FS::UID to run this stuff for us later
52 FS::UID->install_callback( sub {
54 $dir_prefix = $conf->config('home');
55 @shells = $conf->config('shells');
56 $usernamemin = $conf->config('usernamemin') || 2;
57 $usernamemax = $conf->config('usernamemax');
58 $passwordmin = $conf->config('passwordmin') || 6;
59 $passwordmax = $conf->config('passwordmax') || 8;
60 $username_letter = $conf->exists('username-letter');
61 $username_letterfirst = $conf->exists('username-letterfirst');
62 $username_noperiod = $conf->exists('username-noperiod');
63 $username_nounderscore = $conf->exists('username-nounderscore');
64 $username_nodash = $conf->exists('username-nodash');
65 $username_uppercase = $conf->exists('username-uppercase');
66 $username_ampersand = $conf->exists('username-ampersand');
67 $username_percent = $conf->exists('username-percent');
68 $password_noampersand = $conf->exists('password-noexclamation');
69 $password_noexclamation = $conf->exists('password-noexclamation');
70 $dirhash = $conf->config('dirhash') || 0;
71 if ( $conf->exists('warning_email') ) {
72 $warning_template = new Text::Template (
74 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
75 ) or warn "can't create warning email template: $Text::Template::ERROR";
76 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
77 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
78 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
79 $warning_cc = $conf->config('warning_email-cc');
81 $warning_template = '';
83 $warning_subject = '';
84 $warning_mimetype = '';
87 $smtpmachine = $conf->config('smtpmachine');
88 $radius_password = $conf->config('radius-password') || 'Password';
89 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
90 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
94 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
95 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
99 my ( $hashref, $cache ) = @_;
100 if ( $hashref->{'svc_acct_svcnum'} ) {
101 $self->{'_domsvc'} = FS::svc_domain->new( {
102 'svcnum' => $hashref->{'domsvc'},
103 'domain' => $hashref->{'svc_acct_domain'},
104 'catchall' => $hashref->{'svc_acct_catchall'},
111 FS::svc_acct - Object methods for svc_acct records
117 $record = new FS::svc_acct \%hash;
118 $record = new FS::svc_acct { 'column' => 'value' };
120 $error = $record->insert;
122 $error = $new_record->replace($old_record);
124 $error = $record->delete;
126 $error = $record->check;
128 $error = $record->suspend;
130 $error = $record->unsuspend;
132 $error = $record->cancel;
134 %hash = $record->radius;
136 %hash = $record->radius_reply;
138 %hash = $record->radius_check;
140 $domain = $record->domain;
142 $svc_domain = $record->svc_domain;
144 $email = $record->email;
146 $seconds_since = $record->seconds_since($timestamp);
150 An FS::svc_acct object represents an account. FS::svc_acct inherits from
151 FS::svc_Common. The following fields are currently supported:
155 =item svcnum - primary key (assigned automatcially for new accounts)
159 =item _password - generated if blank
161 =item _password_encoding - plain, crypt, ldap (or empty for autodetection)
163 =item sec_phrase - security phrase
165 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
173 =item dir - set automatically if blank (and uid is not)
177 =item quota - (unimplementd)
179 =item slipip - IP address
189 =item domsvc - svcnum from svc_domain
191 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
193 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
203 Creates a new account. To add the account to the database, see L<"insert">.
210 'longname_plural' => 'Access accounts and mailboxes',
211 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
212 'display_weight' => 10,
213 'cancel_weight' => 50,
215 'dir' => 'Home directory',
218 def_label => 'UID (set to fixed and blank for no UIDs)',
221 'slipip' => 'IP address',
222 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
224 label => 'Access number',
226 select_table => 'svc_acct_pop',
227 select_key => 'popnum',
228 select_label => 'city',
234 disable_default => 1,
241 disable_inventory => 1,
244 '_password' => 'Password',
247 def_label => 'GID (when blank, defaults to UID)',
251 #desc =>'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file, set to blank for no shell tracking)',
253 def_label=> 'Shell (set to blank for no shell tracking)',
255 #select_list => [ $conf->config('shells') ],
256 select_list => [ $conf ? $conf->config('shells') : () ],
257 disable_inventory => 1,
260 'finger' => 'Real name', # (GECOS)',
263 #def_label => 'svcnum from svc_domain',
265 select_table => 'svc_domain',
266 select_key => 'svcnum',
267 select_label => 'domain',
268 disable_inventory => 1,
272 label => 'RADIUS groups',
273 type => 'radius_usergroup_selector',
274 disable_inventory => 1,
277 'seconds' => { label => 'Seconds',
278 label_sort => 'with Time Remaining',
280 disable_inventory => 1,
282 disable_part_svc_column => 1,
284 'upbytes' => { label => 'Upload',
286 disable_inventory => 1,
288 'format' => \&FS::UI::bytecount::display_bytecount,
289 'parse' => \&FS::UI::bytecount::parse_bytecount,
290 disable_part_svc_column => 1,
292 'downbytes' => { label => 'Download',
294 disable_inventory => 1,
296 'format' => \&FS::UI::bytecount::display_bytecount,
297 'parse' => \&FS::UI::bytecount::parse_bytecount,
298 disable_part_svc_column => 1,
300 'totalbytes'=> { label => 'Total up and download',
302 disable_inventory => 1,
304 'format' => \&FS::UI::bytecount::display_bytecount,
305 'parse' => \&FS::UI::bytecount::parse_bytecount,
306 disable_part_svc_column => 1,
308 'seconds_threshold' => { label => 'Seconds threshold',
310 disable_inventory => 1,
312 disable_part_svc_column => 1,
314 'upbytes_threshold' => { label => 'Upload threshold',
316 disable_inventory => 1,
318 'format' => \&FS::UI::bytecount::display_bytecount,
319 'parse' => \&FS::UI::bytecount::parse_bytecount,
320 disable_part_svc_column => 1,
322 'downbytes_threshold' => { label => 'Download threshold',
324 disable_inventory => 1,
326 'format' => \&FS::UI::bytecount::display_bytecount,
327 'parse' => \&FS::UI::bytecount::parse_bytecount,
328 disable_part_svc_column => 1,
330 'totalbytes_threshold'=> { label => 'Total up and download threshold',
332 disable_inventory => 1,
334 'format' => \&FS::UI::bytecount::display_bytecount,
335 'parse' => \&FS::UI::bytecount::parse_bytecount,
336 disable_part_svc_column => 1,
339 label => 'Last login',
343 label => 'Last logout',
350 sub table { 'svc_acct'; }
352 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
356 #false laziness with edit/svc_acct.cgi
358 my( $self, $groups ) = @_;
359 if ( ref($groups) eq 'ARRAY' ) {
361 } elsif ( length($groups) ) {
362 [ split(/\s*,\s*/, $groups) ];
371 shift->_lastlog('in', @_);
375 shift->_lastlog('out', @_);
379 my( $self, $op, $time ) = @_;
381 if ( defined($time) ) {
382 warn "$me last_log$op called on svcnum ". $self->svcnum.
383 ' ('. $self->email. "): $time\n"
388 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
392 my $sth = $dbh->prepare( $sql )
393 or die "Error preparing $sql: ". $dbh->errstr;
394 my $rv = $sth->execute($time, $self->svcnum);
395 die "Error executing $sql: ". $sth->errstr
397 die "Can't update last_log$op for svcnum". $self->svcnum
400 $self->{'Hash'}->{"last_log$op"} = $time;
402 $self->getfield("last_log$op");
406 =item search_sql STRING
408 Class method which returns an SQL fragment to search for the given string.
413 my( $class, $string ) = @_;
414 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
415 my( $username, $domain ) = ( $1, $2 );
416 my $q_username = dbh->quote($username);
417 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
419 "svc_acct.username = $q_username AND ( ".
420 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
425 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
427 $class->search_sql_field('slipip', $string ).
429 $class->search_sql_field('username', $string ).
432 $class->search_sql_field('username', $string);
436 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
438 Returns the "username@domain" string for this account.
440 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
450 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
452 Returns a longer string label for this acccount ("Real Name <username@domain>"
453 if available, or "username@domain").
455 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
462 ( $self->finger =~ /\S/ )
463 ? $self->finger. ' <'.$self->label(@_).'>'
467 =item insert [ , OPTION => VALUE ... ]
469 Adds this account to the database. If there is an error, returns the error,
470 otherwise returns false.
472 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
473 defined. An FS::cust_svc record will be created and inserted.
475 The additional field I<usergroup> can optionally be defined; if so it should
476 contain an arrayref of group names. See L<FS::radius_usergroup>.
478 The additional field I<child_objects> can optionally be defined; if so it
479 should contain an arrayref of FS::tablename objects. They will have their
480 svcnum fields set and will be inserted after this record, but before any
481 exports are run. Each element of the array can also optionally be a
482 two-element array reference containing the child object and the name of an
483 alternate field to be filled in with the newly-inserted svcnum, for example
484 C<[ $svc_forward, 'srcsvc' ]>
486 Currently available options are: I<depend_jobnum>
488 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
489 jobnums), all provisioning jobs will have a dependancy on the supplied
490 jobnum(s) (they will not run until the specific job(s) complete(s)).
492 (TODOC: L<FS::queue> and L<freeside-queued>)
494 (TODOC: new exports!)
503 warn "[$me] insert called on $self: ". Dumper($self).
504 "\nwith options: ". Dumper(%options);
507 local $SIG{HUP} = 'IGNORE';
508 local $SIG{INT} = 'IGNORE';
509 local $SIG{QUIT} = 'IGNORE';
510 local $SIG{TERM} = 'IGNORE';
511 local $SIG{TSTP} = 'IGNORE';
512 local $SIG{PIPE} = 'IGNORE';
514 my $oldAutoCommit = $FS::UID::AutoCommit;
515 local $FS::UID::AutoCommit = 0;
518 my $error = $self->check;
519 return $error if $error;
521 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
522 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
523 unless ( $cust_svc ) {
524 $dbh->rollback if $oldAutoCommit;
525 return "no cust_svc record found for svcnum ". $self->svcnum;
527 $self->pkgnum($cust_svc->pkgnum);
528 $self->svcpart($cust_svc->svcpart);
531 # set usage fields and thresholds if unset but set in a package def
532 if ( $self->pkgnum ) {
533 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
534 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
535 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
537 my %values = $part_pkg->usage_valuehash;
538 my $multiplier = $conf->exists('svc_acct-usage_threshold')
539 ? 1 - $conf->config('svc_acct-usage_threshold')/100
542 foreach ( keys %values ) {
543 next if $self->getfield($_);
544 $self->setfield( $_, $values{$_} );
545 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) );
552 $error = $self->SUPER::insert(
553 'jobnums' => \@jobnums,
554 'child_objects' => $self->child_objects,
558 $dbh->rollback if $oldAutoCommit;
562 if ( $self->usergroup ) {
563 foreach my $groupname ( @{$self->usergroup} ) {
564 my $radius_usergroup = new FS::radius_usergroup ( {
565 svcnum => $self->svcnum,
566 groupname => $groupname,
568 my $error = $radius_usergroup->insert;
570 $dbh->rollback if $oldAutoCommit;
576 unless ( $skip_fuzzyfiles ) {
577 $error = $self->queue_fuzzyfiles_update;
579 $dbh->rollback if $oldAutoCommit;
580 return "updating fuzzy search cache: $error";
584 my $cust_pkg = $self->cust_svc->cust_pkg;
587 my $cust_main = $cust_pkg->cust_main;
588 my $agentnum = $cust_main->agentnum;
590 if ( $conf->exists('emailinvoiceautoalways')
591 || $conf->exists('emailinvoiceauto')
592 && ! $cust_main->invoicing_list_emailonly
594 my @invoicing_list = $cust_main->invoicing_list;
595 push @invoicing_list, $self->email;
596 $cust_main->invoicing_list(\@invoicing_list);
600 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
601 = ('','','','','','');
603 if ( $conf->exists('welcome_email', $agentnum) ) {
604 $welcome_template = new Text::Template (
606 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
607 ) or warn "can't create welcome email template: $Text::Template::ERROR";
608 $welcome_from = $conf->config('welcome_email-from', $agentnum);
609 # || 'your-isp-is-dum'
610 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
612 $welcome_subject_template = new Text::Template (
614 SOURCE => $welcome_subject,
615 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
616 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
619 if ( $welcome_template && $cust_pkg ) {
620 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
624 'custnum' => $self->custnum,
625 'username' => $self->username,
626 'password' => $self->_password,
627 'first' => $cust_main->first,
628 'last' => $cust_main->getfield('last'),
629 'pkg' => $cust_pkg->part_pkg->pkg,
631 my $wqueue = new FS::queue {
632 'svcnum' => $self->svcnum,
633 'job' => 'FS::svc_acct::send_email'
635 my $error = $wqueue->insert(
637 'from' => $welcome_from,
638 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
639 'mimetype' => $welcome_mimetype,
640 'body' => $welcome_template->fill_in( HASH => \%hash, ),
643 $dbh->rollback if $oldAutoCommit;
644 return "error queuing welcome email: $error";
647 if ( $options{'depend_jobnum'} ) {
648 warn "$me depend_jobnum found; adding to welcome email dependancies"
650 if ( ref($options{'depend_jobnum'}) ) {
651 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
652 "to welcome email dependancies"
654 push @jobnums, @{ $options{'depend_jobnum'} };
656 warn "$me adding job $options{'depend_jobnum'} ".
657 "to welcome email dependancies"
659 push @jobnums, $options{'depend_jobnum'};
663 foreach my $jobnum ( @jobnums ) {
664 my $error = $wqueue->depend_insert($jobnum);
666 $dbh->rollback if $oldAutoCommit;
667 return "error queuing welcome email job dependancy: $error";
677 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
683 Deletes this account from the database. If there is an error, returns the
684 error, otherwise returns false.
686 The corresponding FS::cust_svc record will be deleted as well.
688 (TODOC: new exports!)
695 return "can't delete system account" if $self->_check_system;
697 return "Can't delete an account which is a (svc_forward) source!"
698 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
700 return "Can't delete an account which is a (svc_forward) destination!"
701 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
703 return "Can't delete an account with (svc_www) web service!"
704 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
706 # what about records in session ? (they should refer to history table)
708 local $SIG{HUP} = 'IGNORE';
709 local $SIG{INT} = 'IGNORE';
710 local $SIG{QUIT} = 'IGNORE';
711 local $SIG{TERM} = 'IGNORE';
712 local $SIG{TSTP} = 'IGNORE';
713 local $SIG{PIPE} = 'IGNORE';
715 my $oldAutoCommit = $FS::UID::AutoCommit;
716 local $FS::UID::AutoCommit = 0;
719 foreach my $cust_main_invoice (
720 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
722 unless ( defined($cust_main_invoice) ) {
723 warn "WARNING: something's wrong with qsearch";
726 my %hash = $cust_main_invoice->hash;
727 $hash{'dest'} = $self->email;
728 my $new = new FS::cust_main_invoice \%hash;
729 my $error = $new->replace($cust_main_invoice);
731 $dbh->rollback if $oldAutoCommit;
736 foreach my $svc_domain (
737 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
739 my %hash = new FS::svc_domain->hash;
740 $hash{'catchall'} = '';
741 my $new = new FS::svc_domain \%hash;
742 my $error = $new->replace($svc_domain);
744 $dbh->rollback if $oldAutoCommit;
749 my $error = $self->SUPER::delete;
751 $dbh->rollback if $oldAutoCommit;
755 foreach my $radius_usergroup (
756 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
758 my $error = $radius_usergroup->delete;
760 $dbh->rollback if $oldAutoCommit;
765 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
769 =item replace OLD_RECORD
771 Replaces OLD_RECORD with this one in the database. If there is an error,
772 returns the error, otherwise returns false.
774 The additional field I<usergroup> can optionally be defined; if so it should
775 contain an arrayref of group names. See L<FS::radius_usergroup>.
783 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
787 warn "$me replacing $old with $new\n" if $DEBUG;
791 return "can't modify system account" if $old->_check_system;
794 #no warnings 'numeric'; #alas, a 5.006-ism
797 foreach my $xid (qw( uid gid )) {
799 return "Can't change $xid!"
800 if ! $conf->exists("svc_acct-edit_$xid")
801 && $old->$xid() != $new->$xid()
802 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
807 #change homdir when we change username
808 $new->setfield('dir', '') if $old->username ne $new->username;
810 local $SIG{HUP} = 'IGNORE';
811 local $SIG{INT} = 'IGNORE';
812 local $SIG{QUIT} = 'IGNORE';
813 local $SIG{TERM} = 'IGNORE';
814 local $SIG{TSTP} = 'IGNORE';
815 local $SIG{PIPE} = 'IGNORE';
817 my $oldAutoCommit = $FS::UID::AutoCommit;
818 local $FS::UID::AutoCommit = 0;
821 # redundant, but so $new->usergroup gets set
822 $error = $new->check;
823 return $error if $error;
825 $old->usergroup( [ $old->radius_groups ] );
827 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
828 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
830 if ( $new->usergroup ) {
831 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
832 my @newgroups = @{$new->usergroup};
833 foreach my $oldgroup ( @{$old->usergroup} ) {
834 if ( grep { $oldgroup eq $_ } @newgroups ) {
835 @newgroups = grep { $oldgroup ne $_ } @newgroups;
838 my $radius_usergroup = qsearchs('radius_usergroup', {
839 svcnum => $old->svcnum,
840 groupname => $oldgroup,
842 my $error = $radius_usergroup->delete;
844 $dbh->rollback if $oldAutoCommit;
845 return "error deleting radius_usergroup $oldgroup: $error";
849 foreach my $newgroup ( @newgroups ) {
850 my $radius_usergroup = new FS::radius_usergroup ( {
851 svcnum => $new->svcnum,
852 groupname => $newgroup,
854 my $error = $radius_usergroup->insert;
856 $dbh->rollback if $oldAutoCommit;
857 return "error adding radius_usergroup $newgroup: $error";
863 $error = $new->SUPER::replace($old, @_);
865 $dbh->rollback if $oldAutoCommit;
866 return $error if $error;
869 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
870 $error = $new->queue_fuzzyfiles_update;
872 $dbh->rollback if $oldAutoCommit;
873 return "updating fuzzy search cache: $error";
877 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
881 =item queue_fuzzyfiles_update
883 Used by insert & replace to update the fuzzy search cache
887 sub queue_fuzzyfiles_update {
890 local $SIG{HUP} = 'IGNORE';
891 local $SIG{INT} = 'IGNORE';
892 local $SIG{QUIT} = 'IGNORE';
893 local $SIG{TERM} = 'IGNORE';
894 local $SIG{TSTP} = 'IGNORE';
895 local $SIG{PIPE} = 'IGNORE';
897 my $oldAutoCommit = $FS::UID::AutoCommit;
898 local $FS::UID::AutoCommit = 0;
901 my $queue = new FS::queue {
902 'svcnum' => $self->svcnum,
903 'job' => 'FS::svc_acct::append_fuzzyfiles'
905 my $error = $queue->insert($self->username);
907 $dbh->rollback if $oldAutoCommit;
908 return "queueing job (transaction rolled back): $error";
911 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
919 Suspends this account by calling export-specific suspend hooks. If there is
920 an error, returns the error, otherwise returns false.
922 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
928 return "can't suspend system account" if $self->_check_system;
929 $self->SUPER::suspend(@_);
934 Unsuspends this account by by calling export-specific suspend hooks. If there
935 is an error, returns the error, otherwise returns false.
937 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
943 my %hash = $self->hash;
944 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
945 $hash{_password} = $1;
946 my $new = new FS::svc_acct ( \%hash );
947 my $error = $new->replace($self);
948 return $error if $error;
951 $self->SUPER::unsuspend(@_);
956 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
958 If the B<auto_unset_catchall> configuration option is set, this method will
959 automatically remove any references to the canceled service in the catchall
960 field of svc_domain. This allows packages that contain both a svc_domain and
961 its catchall svc_acct to be canceled in one step.
966 # Only one thing to do at this level
968 foreach my $svc_domain (
969 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
970 if($conf->exists('auto_unset_catchall')) {
971 my %hash = $svc_domain->hash;
972 $hash{catchall} = '';
973 my $new = new FS::svc_domain ( \%hash );
974 my $error = $new->replace($svc_domain);
975 return $error if $error;
977 return "cannot unprovision svc_acct #".$self->svcnum.
978 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
982 $self->SUPER::cancel(@_);
988 Checks all fields to make sure this is a valid service. If there is an error,
989 returns the error, otherwise returns false. Called by the insert and replace
992 Sets any fixed values; see L<FS::part_svc>.
999 my($recref) = $self->hashref;
1001 my $x = $self->setfixed( $self->_fieldhandlers );
1002 return $x unless ref($x);
1005 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1007 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1010 my $error = $self->ut_numbern('svcnum')
1011 #|| $self->ut_number('domsvc')
1012 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1013 || $self->ut_textn('sec_phrase')
1014 || $self->ut_snumbern('seconds')
1015 || $self->ut_snumbern('upbytes')
1016 || $self->ut_snumbern('downbytes')
1017 || $self->ut_snumbern('totalbytes')
1018 || $self->ut_enum( '_password_encoding',
1019 [ '', qw( plain crypt ldap ) ]
1022 return $error if $error;
1024 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1025 if ( $username_uppercase ) {
1026 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
1027 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1028 $recref->{username} = $1;
1030 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/
1031 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1032 $recref->{username} = $1;
1035 if ( $username_letterfirst ) {
1036 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1037 } elsif ( $username_letter ) {
1038 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1040 if ( $username_noperiod ) {
1041 $recref->{username} =~ /\./ and return gettext('illegal_username');
1043 if ( $username_nounderscore ) {
1044 $recref->{username} =~ /_/ and return gettext('illegal_username');
1046 if ( $username_nodash ) {
1047 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1049 unless ( $username_ampersand ) {
1050 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1052 unless ( $username_percent ) {
1053 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1056 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1057 $recref->{popnum} = $1;
1058 return "Unknown popnum" unless
1059 ! $recref->{popnum} ||
1060 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1062 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1064 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1065 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1067 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1068 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1069 #not all systems use gid=uid
1070 #you can set a fixed gid in part_svc
1072 return "Only root can have uid 0"
1073 if $recref->{uid} == 0
1074 && $recref->{username} !~ /^(root|toor|smtp)$/;
1076 unless ( $recref->{username} eq 'sync' ) {
1077 if ( grep $_ eq $recref->{shell}, @shells ) {
1078 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1080 return "Illegal shell \`". $self->shell. "\'; ".
1081 "shells configuration value contains: @shells";
1084 $recref->{shell} = '/bin/sync';
1088 $recref->{gid} ne '' ?
1089 return "Can't have gid without uid" : ( $recref->{gid}='' );
1090 #$recref->{dir} ne '' ?
1091 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1092 $recref->{shell} ne '' ?
1093 return "Can't have shell without uid" : ( $recref->{shell}='' );
1096 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1098 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1099 or return "Illegal directory: ". $recref->{dir};
1100 $recref->{dir} = $1;
1101 return "Illegal directory"
1102 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1103 return "Illegal directory"
1104 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1105 unless ( $recref->{dir} ) {
1106 $recref->{dir} = $dir_prefix . '/';
1107 if ( $dirhash > 0 ) {
1108 for my $h ( 1 .. $dirhash ) {
1109 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1111 } elsif ( $dirhash < 0 ) {
1112 for my $h ( reverse $dirhash .. -1 ) {
1113 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1116 $recref->{dir} .= $recref->{username};
1122 # $error = $self->ut_textn('finger');
1123 # return $error if $error;
1124 if ( $self->getfield('finger') eq '' ) {
1125 my $cust_pkg = $self->svcnum
1126 ? $self->cust_svc->cust_pkg
1127 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1129 my $cust_main = $cust_pkg->cust_main;
1130 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1133 $self->getfield('finger') =~
1134 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1135 or return "Illegal finger: ". $self->getfield('finger');
1136 $self->setfield('finger', $1);
1138 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1139 $recref->{quota} = $1;
1141 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1142 if ( $recref->{slipip} eq '' ) {
1143 $recref->{slipip} = '';
1144 } elsif ( $recref->{slipip} eq '0e0' ) {
1145 $recref->{slipip} = '0e0';
1147 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1148 or return "Illegal slipip: ". $self->slipip;
1149 $recref->{slipip} = $1;
1154 #arbitrary RADIUS stuff; allow ut_textn for now
1155 foreach ( grep /^radius_/, fields('svc_acct') ) {
1156 $self->ut_textn($_);
1159 if ( $recref->{_password_encoding} eq 'ldap' ) {
1161 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1162 $recref->{_password} = uc($1).$2;
1164 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1167 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1169 if ( $recref->{_password} =~
1170 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1171 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1174 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1177 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1180 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1182 #generate a password if it is blank
1183 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1184 unless length( $recref->{_password} );
1186 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1187 $recref->{_password} = $1;
1189 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1190 FS::Msgcat::_gettext('illegal_password_characters').
1191 ": ". $recref->{_password};
1194 if ( $password_noampersand ) {
1195 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1197 if ( $password_noexclamation ) {
1198 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1203 #carp "warning: _password_encoding unspecified\n";
1205 #generate a password if it is blank
1206 unless ( length( $recref->{_password} ) ) {
1208 $recref->{_password} =
1209 join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1210 $recref->{_password_encoding} = 'plain';
1214 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1215 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1216 $recref->{_password} = $1.$3;
1217 $recref->{_password_encoding} = 'plain';
1218 } elsif ( $recref->{_password} =~
1219 /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/
1221 $recref->{_password} = $1.$3;
1222 $recref->{_password_encoding} = 'crypt';
1223 } elsif ( $recref->{_password} eq '*' ) {
1224 $recref->{_password} = '*';
1225 $recref->{_password_encoding} = 'crypt';
1226 } elsif ( $recref->{_password} eq '!' ) {
1227 $recref->{_password_encoding} = 'crypt';
1228 $recref->{_password} = '!';
1229 } elsif ( $recref->{_password} eq '!!' ) {
1230 $recref->{_password} = '!!';
1231 $recref->{_password_encoding} = 'crypt';
1233 #return "Illegal password";
1234 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1235 FS::Msgcat::_gettext('illegal_password_characters').
1236 ": ". $recref->{_password};
1243 $self->SUPER::check;
1249 Internal function to check the username against the list of system usernames
1250 from the I<system_usernames> configuration value. Returns true if the username
1251 is listed on the system username list.
1257 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1258 $conf->config('system_usernames')
1262 =item _check_duplicate
1264 Internal method to check for duplicates usernames, username@domain pairs and
1267 If the I<global_unique-username> configuration value is set to B<username> or
1268 B<username@domain>, enforces global username or username@domain uniqueness.
1270 In all cases, check for duplicate uids and usernames or username@domain pairs
1271 per export and with identical I<svcpart> values.
1275 sub _check_duplicate {
1278 my $global_unique = $conf->config('global_unique-username') || 'none';
1279 return '' if $global_unique eq 'disabled';
1283 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1284 unless ( $part_svc ) {
1285 return 'unknown svcpart '. $self->svcpart;
1288 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1289 qsearch( 'svc_acct', { 'username' => $self->username } );
1290 return gettext('username_in_use')
1291 if $global_unique eq 'username' && @dup_user;
1293 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1294 qsearch( 'svc_acct', { 'username' => $self->username,
1295 'domsvc' => $self->domsvc } );
1296 return gettext('username_in_use')
1297 if $global_unique eq 'username@domain' && @dup_userdomain;
1300 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1301 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1302 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1303 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1308 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1309 my $exports = FS::part_export::export_info('svc_acct');
1310 my %conflict_user_svcpart;
1311 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1313 foreach my $part_export ( $part_svc->part_export ) {
1315 #this will catch to the same exact export
1316 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1318 #this will catch to exports w/same exporthost+type ???
1319 #my @other_part_export = qsearch('part_export', {
1320 # 'machine' => $part_export->machine,
1321 # 'exporttype' => $part_export->exporttype,
1323 #foreach my $other_part_export ( @other_part_export ) {
1324 # push @svcparts, map { $_->svcpart }
1325 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1328 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1329 #silly kludge to avoid uninitialized value errors
1330 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1331 ? $exports->{$part_export->exporttype}{'nodomain'}
1333 if ( $nodomain =~ /^Y/i ) {
1334 $conflict_user_svcpart{$_} = $part_export->exportnum
1337 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1342 foreach my $dup_user ( @dup_user ) {
1343 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1344 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1345 return "duplicate username ". $self->username.
1346 ": conflicts with svcnum ". $dup_user->svcnum.
1347 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1351 foreach my $dup_userdomain ( @dup_userdomain ) {
1352 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1353 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1354 return "duplicate username\@domain ". $self->email.
1355 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1356 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1360 foreach my $dup_uid ( @dup_uid ) {
1361 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1362 if ( exists($conflict_user_svcpart{$dup_svcpart})
1363 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1364 return "duplicate uid ". $self->uid.
1365 ": conflicts with svcnum ". $dup_uid->svcnum.
1367 ( $conflict_user_svcpart{$dup_svcpart}
1368 || $conflict_userdomain_svcpart{$dup_svcpart} );
1380 Depriciated, use radius_reply instead.
1385 carp "FS::svc_acct::radius depriciated, use radius_reply";
1386 $_[0]->radius_reply;
1391 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1392 reply attributes of this record.
1394 Note that this is now the preferred method for reading RADIUS attributes -
1395 accessing the columns directly is discouraged, as the column names are
1396 expected to change in the future.
1403 return %{ $self->{'radius_reply'} }
1404 if exists $self->{'radius_reply'};
1409 my($column, $attrib) = ($1, $2);
1410 #$attrib =~ s/_/\-/g;
1411 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1412 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1414 if ( $self->slipip && $self->slipip ne '0e0' ) {
1415 $reply{$radius_ip} = $self->slipip;
1418 if ( $self->seconds !~ /^$/ ) {
1419 $reply{'Session-Timeout'} = $self->seconds;
1427 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1428 check attributes of this record.
1430 Note that this is now the preferred method for reading RADIUS attributes -
1431 accessing the columns directly is discouraged, as the column names are
1432 expected to change in the future.
1439 return %{ $self->{'radius_check'} }
1440 if exists $self->{'radius_check'};
1445 my($column, $attrib) = ($1, $2);
1446 #$attrib =~ s/_/\-/g;
1447 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1448 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1451 my($pw_attrib, $password) = $self->radius_password;
1452 $check{$pw_attrib} = $password;
1454 my $cust_svc = $self->cust_svc;
1455 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1457 my $cust_pkg = $cust_svc->cust_pkg;
1458 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1459 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1466 =item radius_password
1468 Returns a key/value pair containing the RADIUS attribute name and value
1473 sub radius_password {
1476 my($pw_attrib, $password);
1477 if ( $self->_password_encoding eq 'ldap' ) {
1479 $pw_attrib = 'Password-With-Header';
1480 $password = $self->_password;
1482 } elsif ( $self->_password_encoding eq 'crypt' ) {
1484 $pw_attrib = 'Crypt-Password';
1485 $password = $self->_password;
1487 } elsif ( $self->_password_encoding eq 'plain' ) {
1489 $pw_attrib = $radius_password; #Cleartext-Password? man rlm_pap
1490 $password = $self->_password;
1494 $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
1495 $password = $self->_password;
1499 ($pw_attrib, $password);
1505 This method instructs the object to "snapshot" or freeze RADIUS check and
1506 reply attributes to the current values.
1510 #bah, my english is too broken this morning
1511 #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
1512 #the FS::cust_pkg's replace method to trigger the correct export updates when
1513 #package dates change)
1518 $self->{$_} = { $self->$_() }
1519 foreach qw( radius_reply radius_check );
1523 =item forget_snapshot
1525 This methos instructs the object to forget any previously snapshotted
1526 RADIUS check and reply attributes.
1530 sub forget_snapshot {
1534 foreach qw( radius_reply radius_check );
1538 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1540 Returns the domain associated with this account.
1542 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1549 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1550 my $svc_domain = $self->svc_domain(@_)
1551 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1552 $svc_domain->domain;
1557 Returns the FS::svc_domain record for this account's domain (see
1562 # FS::h_svc_acct has a history-aware svc_domain override
1567 ? $self->{'_domsvc'}
1568 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1573 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1577 #inherited from svc_Common
1579 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1581 Returns an email address associated with the account.
1583 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1590 $self->username. '@'. $self->domain(@_);
1595 Returns an array of FS::acct_snarf records associated with the account.
1596 If the acct_snarf table does not exist or there are no associated records,
1597 an empty list is returned
1603 return () unless dbdef->table('acct_snarf');
1604 eval "use FS::acct_snarf;";
1606 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1609 =item decrement_upbytes OCTETS
1611 Decrements the I<upbytes> field of this record by the given amount. If there
1612 is an error, returns the error, otherwise returns false.
1616 sub decrement_upbytes {
1617 shift->_op_usage('-', 'upbytes', @_);
1620 =item increment_upbytes OCTETS
1622 Increments the I<upbytes> field of this record by the given amount. If there
1623 is an error, returns the error, otherwise returns false.
1627 sub increment_upbytes {
1628 shift->_op_usage('+', 'upbytes', @_);
1631 =item decrement_downbytes OCTETS
1633 Decrements the I<downbytes> field of this record by the given amount. If there
1634 is an error, returns the error, otherwise returns false.
1638 sub decrement_downbytes {
1639 shift->_op_usage('-', 'downbytes', @_);
1642 =item increment_downbytes OCTETS
1644 Increments the I<downbytes> field of this record by the given amount. If there
1645 is an error, returns the error, otherwise returns false.
1649 sub increment_downbytes {
1650 shift->_op_usage('+', 'downbytes', @_);
1653 =item decrement_totalbytes OCTETS
1655 Decrements the I<totalbytes> field of this record by the given amount. If there
1656 is an error, returns the error, otherwise returns false.
1660 sub decrement_totalbytes {
1661 shift->_op_usage('-', 'totalbytes', @_);
1664 =item increment_totalbytes OCTETS
1666 Increments the I<totalbytes> field of this record by the given amount. If there
1667 is an error, returns the error, otherwise returns false.
1671 sub increment_totalbytes {
1672 shift->_op_usage('+', 'totalbytes', @_);
1675 =item decrement_seconds SECONDS
1677 Decrements the I<seconds> field of this record by the given amount. If there
1678 is an error, returns the error, otherwise returns false.
1682 sub decrement_seconds {
1683 shift->_op_usage('-', 'seconds', @_);
1686 =item increment_seconds SECONDS
1688 Increments the I<seconds> field of this record by the given amount. If there
1689 is an error, returns the error, otherwise returns false.
1693 sub increment_seconds {
1694 shift->_op_usage('+', 'seconds', @_);
1702 my %op2condition = (
1703 '-' => sub { my($self, $column, $amount) = @_;
1704 $self->$column - $amount <= 0;
1706 '+' => sub { my($self, $column, $amount) = @_;
1707 $self->$column + $amount > 0;
1710 my %op2warncondition = (
1711 '-' => sub { my($self, $column, $amount) = @_;
1712 my $threshold = $column . '_threshold';
1713 $self->$column - $amount <= $self->$threshold + 0;
1715 '+' => sub { my($self, $column, $amount) = @_;
1716 $self->$column + $amount > 0;
1721 my( $self, $op, $column, $amount ) = @_;
1723 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1724 ' ('. $self->email. "): $op $amount\n"
1727 return '' unless $amount;
1729 local $SIG{HUP} = 'IGNORE';
1730 local $SIG{INT} = 'IGNORE';
1731 local $SIG{QUIT} = 'IGNORE';
1732 local $SIG{TERM} = 'IGNORE';
1733 local $SIG{TSTP} = 'IGNORE';
1734 local $SIG{PIPE} = 'IGNORE';
1736 my $oldAutoCommit = $FS::UID::AutoCommit;
1737 local $FS::UID::AutoCommit = 0;
1740 my $sql = "UPDATE svc_acct SET $column = ".
1741 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1742 " $op ? WHERE svcnum = ?";
1746 my $sth = $dbh->prepare( $sql )
1747 or die "Error preparing $sql: ". $dbh->errstr;
1748 my $rv = $sth->execute($amount, $self->svcnum);
1749 die "Error executing $sql: ". $sth->errstr
1750 unless defined($rv);
1751 die "Can't update $column for svcnum". $self->svcnum
1754 my $action = $op2action{$op};
1756 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1757 ( $action eq 'suspend' && !$self->overlimit
1758 || $action eq 'unsuspend' && $self->overlimit )
1760 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1761 if ($part_export->option('overlimit_groups')) {
1763 my $other = new FS::svc_acct $self->hashref;
1764 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1765 ($self, $part_export->option('overlimit_groups'));
1766 $other->usergroup( $groups );
1767 if ($action eq 'suspend'){
1768 $new = $other; $old = $self;
1770 $new = $self; $old = $other;
1772 my $error = $part_export->export_replace($new, $old);
1773 $error ||= $self->overlimit($action);
1775 $dbh->rollback if $oldAutoCommit;
1776 return "Error replacing radius groups in export, ${op}: $error";
1782 if ( $conf->exists("svc_acct-usage_$action")
1783 && &{$op2condition{$op}}($self, $column, $amount) ) {
1784 #my $error = $self->$action();
1785 my $error = $self->cust_svc->cust_pkg->$action();
1786 # $error ||= $self->overlimit($action);
1788 $dbh->rollback if $oldAutoCommit;
1789 return "Error ${action}ing: $error";
1793 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1794 my $wqueue = new FS::queue {
1795 'svcnum' => $self->svcnum,
1796 'job' => 'FS::svc_acct::reached_threshold',
1801 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1805 my $error = $wqueue->insert(
1806 'svcnum' => $self->svcnum,
1808 'column' => $column,
1812 $dbh->rollback if $oldAutoCommit;
1813 return "Error queuing threshold activity: $error";
1817 warn "$me update successful; committing\n"
1819 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1825 my( $self, $valueref, %options ) = @_;
1827 warn "$me set_usage called for svcnum ". $self->svcnum.
1828 ' ('. $self->email. "): ".
1829 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1832 local $SIG{HUP} = 'IGNORE';
1833 local $SIG{INT} = 'IGNORE';
1834 local $SIG{QUIT} = 'IGNORE';
1835 local $SIG{TERM} = 'IGNORE';
1836 local $SIG{TSTP} = 'IGNORE';
1837 local $SIG{PIPE} = 'IGNORE';
1839 local $FS::svc_Common::noexport_hack = 1;
1840 my $oldAutoCommit = $FS::UID::AutoCommit;
1841 local $FS::UID::AutoCommit = 0;
1846 if ( $options{null} ) {
1847 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1848 qw( seconds upbytes downbytes totalbytes )
1851 foreach my $field (keys %$valueref){
1852 $reset = 1 if $valueref->{$field};
1853 $self->setfield($field, $valueref->{$field});
1854 $self->setfield( $field.'_threshold',
1855 int($self->getfield($field)
1856 * ( $conf->exists('svc_acct-usage_threshold')
1857 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1862 $handyhash{$field} = $self->getfield($field);
1863 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1865 #my $error = $self->replace; #NO! we avoid the call to ->check for
1866 #die $error if $error; #services not explicity changed via the UI
1868 my $sql = "UPDATE svc_acct SET " .
1869 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1870 " WHERE svcnum = ". $self->svcnum;
1875 if (scalar(keys %handyhash)) {
1876 my $sth = $dbh->prepare( $sql )
1877 or die "Error preparing $sql: ". $dbh->errstr;
1878 my $rv = $sth->execute();
1879 die "Error executing $sql: ". $sth->errstr
1880 unless defined($rv);
1881 die "Can't update usage for svcnum ". $self->svcnum
1888 if ($self->overlimit) {
1889 $error = $self->overlimit('unsuspend');
1890 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1891 if ($part_export->option('overlimit_groups')) {
1892 my $old = new FS::svc_acct $self->hashref;
1893 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1894 ($self, $part_export->option('overlimit_groups'));
1895 $old->usergroup( $groups );
1896 $error ||= $part_export->export_replace($self, $old);
1901 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1902 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1905 $dbh->rollback if $oldAutoCommit;
1906 return "Error unsuspending: $error";
1910 warn "$me update successful; committing\n"
1912 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1918 =item recharge HASHREF
1920 Increments usage columns by the amount specified in HASHREF as
1921 column=>amount pairs.
1926 my ($self, $vhash) = @_;
1929 warn "[$me] recharge called on $self: ". Dumper($self).
1930 "\nwith vhash: ". Dumper($vhash);
1933 my $oldAutoCommit = $FS::UID::AutoCommit;
1934 local $FS::UID::AutoCommit = 0;
1938 foreach my $column (keys %$vhash){
1939 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1943 $dbh->rollback if $oldAutoCommit;
1945 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1950 =item is_rechargeable
1952 Returns true if this svc_account can be "recharged" and false otherwise.
1956 sub is_rechargable {
1958 $self->seconds ne ''
1959 || $self->upbytes ne ''
1960 || $self->downbytes ne ''
1961 || $self->totalbytes ne '';
1964 =item seconds_since TIMESTAMP
1966 Returns the number of seconds this account has been online since TIMESTAMP,
1967 according to the session monitor (see L<FS::Session>).
1969 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1970 L<Time::Local> and L<Date::Parse> for conversion functions.
1974 #note: POD here, implementation in FS::cust_svc
1977 $self->cust_svc->seconds_since(@_);
1980 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1982 Returns the numbers of seconds this account has been online between
1983 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1984 external SQL radacct table, specified via sqlradius export. Sessions which
1985 started in the specified range but are still open are counted from session
1986 start to the end of the range (unless they are over 1 day old, in which case
1987 they are presumed missing their stop record and not counted). Also, sessions
1988 which end in the range but started earlier are counted from the start of the
1989 range to session end. Finally, sessions which start before the range but end
1990 after are counted for the entire range.
1992 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1993 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1998 #note: POD here, implementation in FS::cust_svc
1999 sub seconds_since_sqlradacct {
2001 $self->cust_svc->seconds_since_sqlradacct(@_);
2004 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2006 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2007 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2008 TIMESTAMP_END (exclusive).
2010 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2011 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2016 #note: POD here, implementation in FS::cust_svc
2017 sub attribute_since_sqlradacct {
2019 $self->cust_svc->attribute_since_sqlradacct(@_);
2022 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2024 Returns an array of hash references of this customers login history for the
2025 given time range. (document this better)
2029 sub get_session_history {
2031 $self->cust_svc->get_session_history(@_);
2034 =item last_login_text
2036 Returns text describing the time of last login.
2040 sub last_login_text {
2042 $self->last_login ? ctime($self->last_login) : 'unknown';
2045 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2050 my($self, $start, $end, %opt ) = @_;
2052 my $did = $self->username; #yup
2054 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2056 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2058 #SELECT $for_update * FROM cdr
2059 # WHERE calldate >= $start #need a conversion
2060 # AND calldate < $end #ditto
2061 # AND ( charged_party = "$did"
2062 # OR charged_party = "$prefix$did" #if length($prefix);
2063 # OR ( ( charged_party IS NULL OR charged_party = '' )
2065 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2068 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2071 if ( length($prefix) ) {
2073 " AND ( charged_party = '$did'
2074 OR charged_party = '$prefix$did'
2075 OR ( ( charged_party IS NULL OR charged_party = '' )
2077 ( src = '$did' OR src = '$prefix$did' )
2083 " AND ( charged_party = '$did'
2084 OR ( ( charged_party IS NULL OR charged_party = '' )
2094 'select' => "$for_update *",
2097 #( freesidestatus IS NULL OR freesidestatus = '' )
2098 'freesidestatus' => '',
2100 'extra_sql' => $charged_or_src,
2108 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2114 if ( $self->usergroup ) {
2115 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2116 unless ref($self->usergroup) eq 'ARRAY';
2117 #when provisioning records, export callback runs in svc_Common.pm before
2118 #radius_usergroup records can be inserted...
2119 @{$self->usergroup};
2121 map { $_->groupname }
2122 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2126 =item clone_suspended
2128 Constructor used by FS::part_export::_export_suspend fallback. Document
2133 sub clone_suspended {
2135 my %hash = $self->hash;
2136 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2137 new FS::svc_acct \%hash;
2140 =item clone_kludge_unsuspend
2142 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2147 sub clone_kludge_unsuspend {
2149 my %hash = $self->hash;
2150 $hash{_password} = '';
2151 new FS::svc_acct \%hash;
2154 =item check_password
2156 Checks the supplied password against the (possibly encrypted) password in the
2157 database. Returns true for a successful authentication, false for no match.
2159 Currently supported encryptions are: classic DES crypt() and MD5
2163 sub check_password {
2164 my($self, $check_password) = @_;
2166 #remove old-style SUSPENDED kludge, they should be allowed to login to
2167 #self-service and pay up
2168 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2170 if ( $self->_password_encoding eq 'ldap' ) {
2172 my $auth = from_rfc2307 Authen::Passphrase $self->_password;
2173 return $auth->match($check_password);
2175 } elsif ( $self->_password_encoding eq 'crypt' ) {
2177 my $auth = from_crypt Authen::Passphrase $self->_password;
2178 return $auth->match($check_password);
2180 } elsif ( $self->_password_encoding eq 'plain' ) {
2182 return $check_password eq $password;
2186 #XXX this could be replaced with Authen::Passphrase stuff
2188 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2190 } elsif ( length($password) < 13 ) { #plaintext
2191 $check_password eq $password;
2192 } elsif ( length($password) == 13 ) { #traditional DES crypt
2193 crypt($check_password, $password) eq $password;
2194 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2195 unix_md5_crypt($check_password, $password) eq $password;
2196 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2197 warn "Can't check password: Blowfish encryption not yet supported, ".
2198 "svcnum ". $self->svcnum. "\n";
2201 warn "Can't check password: Unrecognized encryption for svcnum ".
2202 $self->svcnum. "\n";
2210 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2212 Returns an encrypted password, either by passing through an encrypted password
2213 in the database or by encrypting a plaintext password from the database.
2215 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2216 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2217 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2218 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2219 encryption type is only used if the password is not already encrypted in the
2224 sub crypt_password {
2227 if ( $self->_password_encoding eq 'ldap' ) {
2229 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2232 #XXX this could be replaced with Authen::Passphrase stuff
2234 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2235 if ( $encryption eq 'crypt' ) {
2238 $saltset[int(rand(64))].$saltset[int(rand(64))]
2240 } elsif ( $encryption eq 'md5' ) {
2241 unix_md5_crypt( $self->_password );
2242 } elsif ( $encryption eq 'blowfish' ) {
2243 croak "unknown encryption method $encryption";
2245 croak "unknown encryption method $encryption";
2248 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2252 } elsif ( $self->_password_encoding eq 'crypt' ) {
2254 return $self->_password;
2256 } elsif ( $self->_password_encoding eq 'plain' ) {
2258 #XXX this could be replaced with Authen::Passphrase stuff
2260 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2261 if ( $encryption eq 'crypt' ) {
2264 $saltset[int(rand(64))].$saltset[int(rand(64))]
2266 } elsif ( $encryption eq 'md5' ) {
2267 unix_md5_crypt( $self->_password );
2268 } elsif ( $encryption eq 'blowfish' ) {
2269 croak "unknown encryption method $encryption";
2271 croak "unknown encryption method $encryption";
2276 if ( length($self->_password) == 13
2277 || $self->_password =~ /^\$(1|2a?)\$/
2278 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2284 #XXX this could be replaced with Authen::Passphrase stuff
2286 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2287 if ( $encryption eq 'crypt' ) {
2290 $saltset[int(rand(64))].$saltset[int(rand(64))]
2292 } elsif ( $encryption eq 'md5' ) {
2293 unix_md5_crypt( $self->_password );
2294 } elsif ( $encryption eq 'blowfish' ) {
2295 croak "unknown encryption method $encryption";
2297 croak "unknown encryption method $encryption";
2306 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2308 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2309 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2310 "{MD5}5426824942db4253f87a1009fd5d2d4".
2312 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2313 to work the same as the B</crypt_password> method.
2319 #eventually should check a "password-encoding" field
2321 if ( $self->_password_encoding eq 'ldap' ) {
2323 return $self->_password;
2325 } elsif ( $self->_password_encoding eq 'crypt' ) {
2327 if ( length($self->_password) == 13 ) { #crypt
2328 return '{CRYPT}'. $self->_password;
2329 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2331 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2332 # die "Blowfish encryption not supported in this context, svcnum ".
2333 # $self->svcnum. "\n";
2335 warn "encryption method not (yet?) supported in LDAP context";
2336 return '{CRYPT}*'; #unsupported, should not auth
2339 } elsif ( $self->_password_encoding eq 'plain' ) {
2341 return '{PLAIN}'. $self->_password;
2343 #return '{CLEARTEXT}'. $self->_password; #?
2347 if ( length($self->_password) == 13 ) { #crypt
2348 return '{CRYPT}'. $self->_password;
2349 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2351 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2352 warn "Blowfish encryption not supported in this context, svcnum ".
2353 $self->svcnum. "\n";
2356 #are these two necessary anymore?
2357 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2358 return '{SSHA}'. $1;
2359 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2360 return '{NS-MTA-MD5}'. $1;
2363 return '{PLAIN}'. $self->_password;
2365 #return '{CLEARTEXT}'. $self->_password; #?
2367 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2368 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2369 #if ( $encryption eq 'crypt' ) {
2370 # return '{CRYPT}'. crypt(
2372 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2374 #} elsif ( $encryption eq 'md5' ) {
2375 # unix_md5_crypt( $self->_password );
2376 #} elsif ( $encryption eq 'blowfish' ) {
2377 # croak "unknown encryption method $encryption";
2379 # croak "unknown encryption method $encryption";
2387 =item domain_slash_username
2389 Returns $domain/$username/
2393 sub domain_slash_username {
2395 $self->domain. '/'. $self->username. '/';
2398 =item virtual_maildir
2400 Returns $domain/maildirs/$username/
2404 sub virtual_maildir {
2406 $self->domain. '/maildirs/'. $self->username. '/';
2417 This is the FS::svc_acct job-queue-able version. It still uses
2418 FS::Misc::send_email under-the-hood.
2425 eval "use FS::Misc qw(send_email)";
2428 $opt{mimetype} ||= 'text/plain';
2429 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2431 my $error = send_email(
2432 'from' => $opt{from},
2434 'subject' => $opt{subject},
2435 'content-type' => $opt{mimetype},
2436 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2438 die $error if $error;
2441 =item check_and_rebuild_fuzzyfiles
2445 sub check_and_rebuild_fuzzyfiles {
2446 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2447 -e "$dir/svc_acct.username"
2448 or &rebuild_fuzzyfiles;
2451 =item rebuild_fuzzyfiles
2455 sub rebuild_fuzzyfiles {
2457 use Fcntl qw(:flock);
2459 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2463 open(USERNAMELOCK,">>$dir/svc_acct.username")
2464 or die "can't open $dir/svc_acct.username: $!";
2465 flock(USERNAMELOCK,LOCK_EX)
2466 or die "can't lock $dir/svc_acct.username: $!";
2468 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2470 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2471 or die "can't open $dir/svc_acct.username.tmp: $!";
2472 print USERNAMECACHE join("\n", @all_username), "\n";
2473 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2475 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2485 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2486 open(USERNAMECACHE,"<$dir/svc_acct.username")
2487 or die "can't open $dir/svc_acct.username: $!";
2488 my @array = map { chomp; $_; } <USERNAMECACHE>;
2489 close USERNAMECACHE;
2493 =item append_fuzzyfiles USERNAME
2497 sub append_fuzzyfiles {
2498 my $username = shift;
2500 &check_and_rebuild_fuzzyfiles;
2502 use Fcntl qw(:flock);
2504 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2506 open(USERNAME,">>$dir/svc_acct.username")
2507 or die "can't open $dir/svc_acct.username: $!";
2508 flock(USERNAME,LOCK_EX)
2509 or die "can't lock $dir/svc_acct.username: $!";
2511 print USERNAME "$username\n";
2513 flock(USERNAME,LOCK_UN)
2514 or die "can't unlock $dir/svc_acct.username: $!";
2522 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2526 sub radius_usergroup_selector {
2527 my $sel_groups = shift;
2528 my %sel_groups = map { $_=>1 } @$sel_groups;
2530 my $selectname = shift || 'radius_usergroup';
2533 my $sth = $dbh->prepare(
2534 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2535 ) or die $dbh->errstr;
2536 $sth->execute() or die $sth->errstr;
2537 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2541 function ${selectname}_doadd(object) {
2542 var myvalue = object.${selectname}_add.value;
2543 var optionName = new Option(myvalue,myvalue,false,true);
2544 var length = object.$selectname.length;
2545 object.$selectname.options[length] = optionName;
2546 object.${selectname}_add.value = "";
2549 <SELECT MULTIPLE NAME="$selectname">
2552 foreach my $group ( @all_groups ) {
2553 $html .= qq(<OPTION VALUE="$group");
2554 if ( $sel_groups{$group} ) {
2555 $html .= ' SELECTED';
2556 $sel_groups{$group} = 0;
2558 $html .= ">$group</OPTION>\n";
2560 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2561 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2563 $html .= '</SELECT>';
2565 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2566 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2571 =item reached_threshold
2573 Performs some activities when svc_acct thresholds (such as number of seconds
2574 remaining) are reached.
2578 sub reached_threshold {
2581 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2582 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2584 if ( $opt{'op'} eq '+' ){
2585 $svc_acct->setfield( $opt{'column'}.'_threshold',
2586 int($svc_acct->getfield($opt{'column'})
2587 * ( $conf->exists('svc_acct-usage_threshold')
2588 ? $conf->config('svc_acct-usage_threshold')/100
2593 my $error = $svc_acct->replace;
2594 die $error if $error;
2595 }elsif ( $opt{'op'} eq '-' ){
2597 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2598 return '' if ($threshold eq '' );
2600 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2601 my $error = $svc_acct->replace;
2602 die $error if $error; # email next time, i guess
2604 if ( $warning_template ) {
2605 eval "use FS::Misc qw(send_email)";
2608 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2609 my $cust_main = $cust_pkg->cust_main;
2611 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2612 $cust_main->invoicing_list,
2613 ($opt{'to'} ? $opt{'to'} : ())
2616 my $mimetype = $warning_mimetype;
2617 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2619 my $body = $warning_template->fill_in( HASH => {
2620 'custnum' => $cust_main->custnum,
2621 'username' => $svc_acct->username,
2622 'password' => $svc_acct->_password,
2623 'first' => $cust_main->first,
2624 'last' => $cust_main->getfield('last'),
2625 'pkg' => $cust_pkg->part_pkg->pkg,
2626 'column' => $opt{'column'},
2627 'amount' => $opt{'column'} =~/bytes/
2628 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2629 : $svc_acct->getfield($opt{'column'}),
2630 'threshold' => $opt{'column'} =~/bytes/
2631 ? FS::UI::bytecount::display_bytecount($threshold)
2636 my $error = send_email(
2637 'from' => $warning_from,
2639 'subject' => $warning_subject,
2640 'content-type' => $mimetype,
2641 'body' => [ map "$_\n", split("\n", $body) ],
2643 die $error if $error;
2646 die "unknown op: " . $opt{'op'};
2654 The $recref stuff in sub check should be cleaned up.
2656 The suspend, unsuspend and cancel methods update the database, but not the
2657 current object. This is probably a bug as it's unexpected and
2660 radius_usergroup_selector? putting web ui components in here? they should
2661 probably live somewhere else...
2663 insertion of RADIUS group stuff in insert could be done with child_objects now
2664 (would probably clean up export of them too)
2668 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2669 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2670 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2671 L<freeside-queued>), L<FS::svc_acct_pop>,
2672 schema.html from the base documentation.
2676 =item domain_select_hash %OPTIONS
2678 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2679 may at present purchase.
2681 Currently available options are: I<pkgnum> I<svcpart>
2685 sub domain_select_hash {
2686 my ($self, %options) = @_;
2692 $part_svc = $self->part_svc;
2693 $cust_pkg = $self->cust_svc->cust_pkg
2697 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2698 if $options{'svcpart'};
2700 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2701 if $options{'pkgnum'};
2703 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2704 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2705 %domains = map { $_->svcnum => $_->domain }
2706 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2707 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2708 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2709 %domains = map { $_->svcnum => $_->domain }
2710 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2711 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2712 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2714 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2717 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2718 my $svc_domain = qsearchs('svc_domain',
2719 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2720 if ( $svc_domain ) {
2721 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2723 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2724 $part_svc->part_svc_column('domsvc')->columnvalue;