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;
23 use Authen::Passphrase;
24 use FS::UID qw( datasrc driver_name );
26 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
27 use FS::Msgcat qw(gettext);
28 use FS::UI::bytecount;
34 use FS::cust_main_invoice;
38 use FS::radius_usergroup;
45 @ISA = qw( FS::svc_Common );
48 $me = '[FS::svc_acct]';
50 #ask FS::UID to run this stuff for us later
51 FS::UID->install_callback( sub {
53 $dir_prefix = $conf->config('home');
54 @shells = $conf->config('shells');
55 $usernamemin = $conf->config('usernamemin') || 2;
56 $usernamemax = $conf->config('usernamemax');
57 $passwordmin = $conf->config('passwordmin') || 6;
58 $passwordmax = $conf->config('passwordmax') || 8;
59 $username_letter = $conf->exists('username-letter');
60 $username_letterfirst = $conf->exists('username-letterfirst');
61 $username_noperiod = $conf->exists('username-noperiod');
62 $username_nounderscore = $conf->exists('username-nounderscore');
63 $username_nodash = $conf->exists('username-nodash');
64 $username_uppercase = $conf->exists('username-uppercase');
65 $username_ampersand = $conf->exists('username-ampersand');
66 $username_percent = $conf->exists('username-percent');
67 $password_noampersand = $conf->exists('password-noexclamation');
68 $password_noexclamation = $conf->exists('password-noexclamation');
69 $dirhash = $conf->config('dirhash') || 0;
70 if ( $conf->exists('warning_email') ) {
71 $warning_template = new Text::Template (
73 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
74 ) or warn "can't create warning email template: $Text::Template::ERROR";
75 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
76 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
77 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
78 $warning_cc = $conf->config('warning_email-cc');
80 $warning_template = '';
82 $warning_subject = '';
83 $warning_mimetype = '';
86 $smtpmachine = $conf->config('smtpmachine');
87 $radius_password = $conf->config('radius-password') || 'Password';
88 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
89 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
93 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
94 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
98 my ( $hashref, $cache ) = @_;
99 if ( $hashref->{'svc_acct_svcnum'} ) {
100 $self->{'_domsvc'} = FS::svc_domain->new( {
101 'svcnum' => $hashref->{'domsvc'},
102 'domain' => $hashref->{'svc_acct_domain'},
103 'catchall' => $hashref->{'svc_acct_catchall'},
110 FS::svc_acct - Object methods for svc_acct records
116 $record = new FS::svc_acct \%hash;
117 $record = new FS::svc_acct { 'column' => 'value' };
119 $error = $record->insert;
121 $error = $new_record->replace($old_record);
123 $error = $record->delete;
125 $error = $record->check;
127 $error = $record->suspend;
129 $error = $record->unsuspend;
131 $error = $record->cancel;
133 %hash = $record->radius;
135 %hash = $record->radius_reply;
137 %hash = $record->radius_check;
139 $domain = $record->domain;
141 $svc_domain = $record->svc_domain;
143 $email = $record->email;
145 $seconds_since = $record->seconds_since($timestamp);
149 An FS::svc_acct object represents an account. FS::svc_acct inherits from
150 FS::svc_Common. The following fields are currently supported:
154 =item svcnum - primary key (assigned automatcially for new accounts)
158 =item _password - generated if blank
160 =item _password_encoding - plain, crypt, ldap (or empty for autodetection)
162 =item sec_phrase - security phrase
164 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
172 =item dir - set automatically if blank (and uid is not)
176 =item quota - (unimplementd)
178 =item slipip - IP address
188 =item domsvc - svcnum from svc_domain
190 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
192 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
202 Creates a new account. To add the account to the database, see L<"insert">.
209 'longname_plural' => 'Access accounts and mailboxes',
210 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
211 'display_weight' => 10,
212 'cancel_weight' => 50,
214 'dir' => 'Home directory',
217 def_label => 'UID (set to fixed and blank for no UIDs)',
220 'slipip' => 'IP address',
221 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
223 label => 'Access number',
225 select_table => 'svc_acct_pop',
226 select_key => 'popnum',
227 select_label => 'city',
233 disable_default => 1,
240 disable_inventory => 1,
243 '_password' => 'Password',
246 def_label => 'GID (when blank, defaults to UID)',
250 #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)',
252 def_label=> 'Shell (set to blank for no shell tracking)',
254 #select_list => [ $conf->config('shells') ],
255 select_list => [ $conf ? $conf->config('shells') : () ],
256 disable_inventory => 1,
259 'finger' => 'Real name (GECOS)',
262 #def_label => 'svcnum from svc_domain',
264 select_table => 'svc_domain',
265 select_key => 'svcnum',
266 select_label => 'domain',
267 disable_inventory => 1,
271 label => 'RADIUS groups',
272 type => 'radius_usergroup_selector',
273 disable_inventory => 1,
276 'seconds' => { label => 'Seconds',
277 label_sort => 'with Time Remaining',
279 disable_inventory => 1,
281 disable_part_svc_column => 1,
283 'upbytes' => { label => 'Upload',
285 disable_inventory => 1,
287 'format' => \&FS::UI::bytecount::display_bytecount,
288 'parse' => \&FS::UI::bytecount::parse_bytecount,
289 disable_part_svc_column => 1,
291 'downbytes' => { label => 'Download',
293 disable_inventory => 1,
295 'format' => \&FS::UI::bytecount::display_bytecount,
296 'parse' => \&FS::UI::bytecount::parse_bytecount,
297 disable_part_svc_column => 1,
299 'totalbytes'=> { label => 'Total up and download',
301 disable_inventory => 1,
303 'format' => \&FS::UI::bytecount::display_bytecount,
304 'parse' => \&FS::UI::bytecount::parse_bytecount,
305 disable_part_svc_column => 1,
307 'seconds_threshold' => { label => 'Seconds threshold',
309 disable_inventory => 1,
311 disable_part_svc_column => 1,
313 'upbytes_threshold' => { label => 'Upload threshold',
315 disable_inventory => 1,
317 'format' => \&FS::UI::bytecount::display_bytecount,
318 'parse' => \&FS::UI::bytecount::parse_bytecount,
319 disable_part_svc_column => 1,
321 'downbytes_threshold' => { label => 'Download threshold',
323 disable_inventory => 1,
325 'format' => \&FS::UI::bytecount::display_bytecount,
326 'parse' => \&FS::UI::bytecount::parse_bytecount,
327 disable_part_svc_column => 1,
329 'totalbytes_threshold'=> { label => 'Total up and download threshold',
331 disable_inventory => 1,
333 'format' => \&FS::UI::bytecount::display_bytecount,
334 'parse' => \&FS::UI::bytecount::parse_bytecount,
335 disable_part_svc_column => 1,
338 label => 'Last login',
342 label => 'Last logout',
349 sub table { 'svc_acct'; }
351 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
355 #false laziness with edit/svc_acct.cgi
357 my( $self, $groups ) = @_;
358 if ( ref($groups) eq 'ARRAY' ) {
360 } elsif ( length($groups) ) {
361 [ split(/\s*,\s*/, $groups) ];
370 shift->_lastlog('in', @_);
374 shift->_lastlog('out', @_);
378 my( $self, $op, $time ) = @_;
380 if ( defined($time) ) {
381 warn "$me last_log$op called on svcnum ". $self->svcnum.
382 ' ('. $self->email. "): $time\n"
387 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
391 my $sth = $dbh->prepare( $sql )
392 or die "Error preparing $sql: ". $dbh->errstr;
393 my $rv = $sth->execute($time, $self->svcnum);
394 die "Error executing $sql: ". $sth->errstr
396 die "Can't update last_log$op for svcnum". $self->svcnum
399 $self->{'Hash'}->{"last_log$op"} = $time;
401 $self->getfield("last_log$op");
405 =item search_sql STRING
407 Class method which returns an SQL fragment to search for the given string.
412 my( $class, $string ) = @_;
413 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
414 my( $username, $domain ) = ( $1, $2 );
415 my $q_username = dbh->quote($username);
416 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
418 "svc_acct.username = $q_username AND ( ".
419 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
424 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
426 $class->search_sql_field('slipip', $string ).
428 $class->search_sql_field('username', $string ).
431 $class->search_sql_field('username', $string);
435 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
437 Returns the "username@domain" string for this account.
439 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
451 =item insert [ , OPTION => VALUE ... ]
453 Adds this account to the database. If there is an error, returns the error,
454 otherwise returns false.
456 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
457 defined. An FS::cust_svc record will be created and inserted.
459 The additional field I<usergroup> can optionally be defined; if so it should
460 contain an arrayref of group names. See L<FS::radius_usergroup>.
462 The additional field I<child_objects> can optionally be defined; if so it
463 should contain an arrayref of FS::tablename objects. They will have their
464 svcnum fields set and will be inserted after this record, but before any
465 exports are run. Each element of the array can also optionally be a
466 two-element array reference containing the child object and the name of an
467 alternate field to be filled in with the newly-inserted svcnum, for example
468 C<[ $svc_forward, 'srcsvc' ]>
470 Currently available options are: I<depend_jobnum>
472 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
473 jobnums), all provisioning jobs will have a dependancy on the supplied
474 jobnum(s) (they will not run until the specific job(s) complete(s)).
476 (TODOC: L<FS::queue> and L<freeside-queued>)
478 (TODOC: new exports!)
487 warn "[$me] insert called on $self: ". Dumper($self).
488 "\nwith options: ". Dumper(%options);
491 local $SIG{HUP} = 'IGNORE';
492 local $SIG{INT} = 'IGNORE';
493 local $SIG{QUIT} = 'IGNORE';
494 local $SIG{TERM} = 'IGNORE';
495 local $SIG{TSTP} = 'IGNORE';
496 local $SIG{PIPE} = 'IGNORE';
498 my $oldAutoCommit = $FS::UID::AutoCommit;
499 local $FS::UID::AutoCommit = 0;
502 my $error = $self->check;
503 return $error if $error;
505 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
506 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
507 unless ( $cust_svc ) {
508 $dbh->rollback if $oldAutoCommit;
509 return "no cust_svc record found for svcnum ". $self->svcnum;
511 $self->pkgnum($cust_svc->pkgnum);
512 $self->svcpart($cust_svc->svcpart);
515 # set usage fields and thresholds if unset but set in a package def
516 if ( $self->pkgnum ) {
517 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
518 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
519 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
521 my %values = $part_pkg->usage_valuehash;
522 my $multiplier = $conf->exists('svc_acct-usage_threshold')
523 ? 1 - $conf->config('svc_acct-usage_threshold')/100
526 foreach ( keys %values ) {
527 next if $self->getfield($_);
528 $self->setfield( $_, $values{$_} );
529 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) );
536 $error = $self->SUPER::insert(
537 'jobnums' => \@jobnums,
538 'child_objects' => $self->child_objects,
542 $dbh->rollback if $oldAutoCommit;
546 if ( $self->usergroup ) {
547 foreach my $groupname ( @{$self->usergroup} ) {
548 my $radius_usergroup = new FS::radius_usergroup ( {
549 svcnum => $self->svcnum,
550 groupname => $groupname,
552 my $error = $radius_usergroup->insert;
554 $dbh->rollback if $oldAutoCommit;
560 unless ( $skip_fuzzyfiles ) {
561 $error = $self->queue_fuzzyfiles_update;
563 $dbh->rollback if $oldAutoCommit;
564 return "updating fuzzy search cache: $error";
568 my $cust_pkg = $self->cust_svc->cust_pkg;
571 my $cust_main = $cust_pkg->cust_main;
572 my $agentnum = $cust_main->agentnum;
574 if ( $conf->exists('emailinvoiceautoalways')
575 || $conf->exists('emailinvoiceauto')
576 && ! $cust_main->invoicing_list_emailonly
578 my @invoicing_list = $cust_main->invoicing_list;
579 push @invoicing_list, $self->email;
580 $cust_main->invoicing_list(\@invoicing_list);
584 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
585 = ('','','','','','');
587 if ( $conf->exists('welcome_email', $agentnum) ) {
588 $welcome_template = new Text::Template (
590 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
591 ) or warn "can't create welcome email template: $Text::Template::ERROR";
592 $welcome_from = $conf->config('welcome_email-from', $agentnum);
593 # || 'your-isp-is-dum'
594 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
596 $welcome_subject_template = new Text::Template (
598 SOURCE => $welcome_subject,
599 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
600 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
603 if ( $welcome_template && $cust_pkg ) {
604 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
608 'custnum' => $self->custnum,
609 'username' => $self->username,
610 'password' => $self->_password,
611 'first' => $cust_main->first,
612 'last' => $cust_main->getfield('last'),
613 'pkg' => $cust_pkg->part_pkg->pkg,
615 my $wqueue = new FS::queue {
616 'svcnum' => $self->svcnum,
617 'job' => 'FS::svc_acct::send_email'
619 my $error = $wqueue->insert(
621 'from' => $welcome_from,
622 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
623 'mimetype' => $welcome_mimetype,
624 'body' => $welcome_template->fill_in( HASH => \%hash, ),
627 $dbh->rollback if $oldAutoCommit;
628 return "error queuing welcome email: $error";
631 if ( $options{'depend_jobnum'} ) {
632 warn "$me depend_jobnum found; adding to welcome email dependancies"
634 if ( ref($options{'depend_jobnum'}) ) {
635 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
636 "to welcome email dependancies"
638 push @jobnums, @{ $options{'depend_jobnum'} };
640 warn "$me adding job $options{'depend_jobnum'} ".
641 "to welcome email dependancies"
643 push @jobnums, $options{'depend_jobnum'};
647 foreach my $jobnum ( @jobnums ) {
648 my $error = $wqueue->depend_insert($jobnum);
650 $dbh->rollback if $oldAutoCommit;
651 return "error queuing welcome email job dependancy: $error";
661 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
667 Deletes this account from the database. If there is an error, returns the
668 error, otherwise returns false.
670 The corresponding FS::cust_svc record will be deleted as well.
672 (TODOC: new exports!)
679 return "can't delete system account" if $self->_check_system;
681 return "Can't delete an account which is a (svc_forward) source!"
682 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
684 return "Can't delete an account which is a (svc_forward) destination!"
685 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
687 return "Can't delete an account with (svc_www) web service!"
688 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
690 # what about records in session ? (they should refer to history table)
692 local $SIG{HUP} = 'IGNORE';
693 local $SIG{INT} = 'IGNORE';
694 local $SIG{QUIT} = 'IGNORE';
695 local $SIG{TERM} = 'IGNORE';
696 local $SIG{TSTP} = 'IGNORE';
697 local $SIG{PIPE} = 'IGNORE';
699 my $oldAutoCommit = $FS::UID::AutoCommit;
700 local $FS::UID::AutoCommit = 0;
703 foreach my $cust_main_invoice (
704 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
706 unless ( defined($cust_main_invoice) ) {
707 warn "WARNING: something's wrong with qsearch";
710 my %hash = $cust_main_invoice->hash;
711 $hash{'dest'} = $self->email;
712 my $new = new FS::cust_main_invoice \%hash;
713 my $error = $new->replace($cust_main_invoice);
715 $dbh->rollback if $oldAutoCommit;
720 foreach my $svc_domain (
721 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
723 my %hash = new FS::svc_domain->hash;
724 $hash{'catchall'} = '';
725 my $new = new FS::svc_domain \%hash;
726 my $error = $new->replace($svc_domain);
728 $dbh->rollback if $oldAutoCommit;
733 my $error = $self->SUPER::delete;
735 $dbh->rollback if $oldAutoCommit;
739 foreach my $radius_usergroup (
740 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
742 my $error = $radius_usergroup->delete;
744 $dbh->rollback if $oldAutoCommit;
749 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
753 =item replace OLD_RECORD
755 Replaces OLD_RECORD with this one in the database. If there is an error,
756 returns the error, otherwise returns false.
758 The additional field I<usergroup> can optionally be defined; if so it should
759 contain an arrayref of group names. See L<FS::radius_usergroup>.
767 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
771 warn "$me replacing $old with $new\n" if $DEBUG;
775 return "can't modify system account" if $old->_check_system;
778 #no warnings 'numeric'; #alas, a 5.006-ism
781 foreach my $xid (qw( uid gid )) {
783 return "Can't change $xid!"
784 if ! $conf->exists("svc_acct-edit_$xid")
785 && $old->$xid() != $new->$xid()
786 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
791 #change homdir when we change username
792 $new->setfield('dir', '') if $old->username ne $new->username;
794 local $SIG{HUP} = 'IGNORE';
795 local $SIG{INT} = 'IGNORE';
796 local $SIG{QUIT} = 'IGNORE';
797 local $SIG{TERM} = 'IGNORE';
798 local $SIG{TSTP} = 'IGNORE';
799 local $SIG{PIPE} = 'IGNORE';
801 my $oldAutoCommit = $FS::UID::AutoCommit;
802 local $FS::UID::AutoCommit = 0;
805 # redundant, but so $new->usergroup gets set
806 $error = $new->check;
807 return $error if $error;
809 $old->usergroup( [ $old->radius_groups ] );
811 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
812 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
814 if ( $new->usergroup ) {
815 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
816 my @newgroups = @{$new->usergroup};
817 foreach my $oldgroup ( @{$old->usergroup} ) {
818 if ( grep { $oldgroup eq $_ } @newgroups ) {
819 @newgroups = grep { $oldgroup ne $_ } @newgroups;
822 my $radius_usergroup = qsearchs('radius_usergroup', {
823 svcnum => $old->svcnum,
824 groupname => $oldgroup,
826 my $error = $radius_usergroup->delete;
828 $dbh->rollback if $oldAutoCommit;
829 return "error deleting radius_usergroup $oldgroup: $error";
833 foreach my $newgroup ( @newgroups ) {
834 my $radius_usergroup = new FS::radius_usergroup ( {
835 svcnum => $new->svcnum,
836 groupname => $newgroup,
838 my $error = $radius_usergroup->insert;
840 $dbh->rollback if $oldAutoCommit;
841 return "error adding radius_usergroup $newgroup: $error";
847 $error = $new->SUPER::replace($old, @_);
849 $dbh->rollback if $oldAutoCommit;
850 return $error if $error;
853 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
854 $error = $new->queue_fuzzyfiles_update;
856 $dbh->rollback if $oldAutoCommit;
857 return "updating fuzzy search cache: $error";
861 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
865 =item queue_fuzzyfiles_update
867 Used by insert & replace to update the fuzzy search cache
871 sub queue_fuzzyfiles_update {
874 local $SIG{HUP} = 'IGNORE';
875 local $SIG{INT} = 'IGNORE';
876 local $SIG{QUIT} = 'IGNORE';
877 local $SIG{TERM} = 'IGNORE';
878 local $SIG{TSTP} = 'IGNORE';
879 local $SIG{PIPE} = 'IGNORE';
881 my $oldAutoCommit = $FS::UID::AutoCommit;
882 local $FS::UID::AutoCommit = 0;
885 my $queue = new FS::queue {
886 'svcnum' => $self->svcnum,
887 'job' => 'FS::svc_acct::append_fuzzyfiles'
889 my $error = $queue->insert($self->username);
891 $dbh->rollback if $oldAutoCommit;
892 return "queueing job (transaction rolled back): $error";
895 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
903 Suspends this account by calling export-specific suspend hooks. If there is
904 an error, returns the error, otherwise returns false.
906 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
912 return "can't suspend system account" if $self->_check_system;
913 $self->SUPER::suspend(@_);
918 Unsuspends this account by by calling export-specific suspend hooks. If there
919 is an error, returns the error, otherwise returns false.
921 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
927 my %hash = $self->hash;
928 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
929 $hash{_password} = $1;
930 my $new = new FS::svc_acct ( \%hash );
931 my $error = $new->replace($self);
932 return $error if $error;
935 $self->SUPER::unsuspend(@_);
940 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
942 If the B<auto_unset_catchall> configuration option is set, this method will
943 automatically remove any references to the canceled service in the catchall
944 field of svc_domain. This allows packages that contain both a svc_domain and
945 its catchall svc_acct to be canceled in one step.
950 # Only one thing to do at this level
952 foreach my $svc_domain (
953 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
954 if($conf->exists('auto_unset_catchall')) {
955 my %hash = $svc_domain->hash;
956 $hash{catchall} = '';
957 my $new = new FS::svc_domain ( \%hash );
958 my $error = $new->replace($svc_domain);
959 return $error if $error;
961 return "cannot unprovision svc_acct #".$self->svcnum.
962 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
966 $self->SUPER::cancel(@_);
972 Checks all fields to make sure this is a valid service. If there is an error,
973 returns the error, otherwise returns false. Called by the insert and replace
976 Sets any fixed values; see L<FS::part_svc>.
983 my($recref) = $self->hashref;
985 my $x = $self->setfixed( $self->_fieldhandlers );
986 return $x unless ref($x);
989 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
991 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
994 my $error = $self->ut_numbern('svcnum')
995 #|| $self->ut_number('domsvc')
996 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
997 || $self->ut_textn('sec_phrase')
998 || $self->ut_snumbern('seconds')
999 || $self->ut_snumbern('upbytes')
1000 || $self->ut_snumbern('downbytes')
1001 || $self->ut_snumbern('totalbytes')
1002 || $self->ut_enum( '_password_encoding',
1003 [ '', qw( plain crypt ldap ) ]
1006 return $error if $error;
1008 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1009 if ( $username_uppercase ) {
1010 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
1011 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1012 $recref->{username} = $1;
1014 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/
1015 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1016 $recref->{username} = $1;
1019 if ( $username_letterfirst ) {
1020 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1021 } elsif ( $username_letter ) {
1022 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1024 if ( $username_noperiod ) {
1025 $recref->{username} =~ /\./ and return gettext('illegal_username');
1027 if ( $username_nounderscore ) {
1028 $recref->{username} =~ /_/ and return gettext('illegal_username');
1030 if ( $username_nodash ) {
1031 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1033 unless ( $username_ampersand ) {
1034 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1036 unless ( $username_percent ) {
1037 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1040 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1041 $recref->{popnum} = $1;
1042 return "Unknown popnum" unless
1043 ! $recref->{popnum} ||
1044 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1046 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1048 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1049 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1051 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1052 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1053 #not all systems use gid=uid
1054 #you can set a fixed gid in part_svc
1056 return "Only root can have uid 0"
1057 if $recref->{uid} == 0
1058 && $recref->{username} !~ /^(root|toor|smtp)$/;
1060 unless ( $recref->{username} eq 'sync' ) {
1061 if ( grep $_ eq $recref->{shell}, @shells ) {
1062 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1064 return "Illegal shell \`". $self->shell. "\'; ".
1065 "shells configuration value contains: @shells";
1068 $recref->{shell} = '/bin/sync';
1072 $recref->{gid} ne '' ?
1073 return "Can't have gid without uid" : ( $recref->{gid}='' );
1074 #$recref->{dir} ne '' ?
1075 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1076 $recref->{shell} ne '' ?
1077 return "Can't have shell without uid" : ( $recref->{shell}='' );
1080 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1082 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1083 or return "Illegal directory: ". $recref->{dir};
1084 $recref->{dir} = $1;
1085 return "Illegal directory"
1086 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1087 return "Illegal directory"
1088 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1089 unless ( $recref->{dir} ) {
1090 $recref->{dir} = $dir_prefix . '/';
1091 if ( $dirhash > 0 ) {
1092 for my $h ( 1 .. $dirhash ) {
1093 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1095 } elsif ( $dirhash < 0 ) {
1096 for my $h ( reverse $dirhash .. -1 ) {
1097 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1100 $recref->{dir} .= $recref->{username};
1106 # $error = $self->ut_textn('finger');
1107 # return $error if $error;
1108 if ( $self->getfield('finger') eq '' ) {
1109 my $cust_pkg = $self->svcnum
1110 ? $self->cust_svc->cust_pkg
1111 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1113 my $cust_main = $cust_pkg->cust_main;
1114 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1117 $self->getfield('finger') =~
1118 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1119 or return "Illegal finger: ". $self->getfield('finger');
1120 $self->setfield('finger', $1);
1122 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1123 $recref->{quota} = $1;
1125 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1126 if ( $recref->{slipip} eq '' ) {
1127 $recref->{slipip} = '';
1128 } elsif ( $recref->{slipip} eq '0e0' ) {
1129 $recref->{slipip} = '0e0';
1131 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1132 or return "Illegal slipip: ". $self->slipip;
1133 $recref->{slipip} = $1;
1138 #arbitrary RADIUS stuff; allow ut_textn for now
1139 foreach ( grep /^radius_/, fields('svc_acct') ) {
1140 $self->ut_textn($_);
1143 if ( $recref->{_password_encoding} eq 'ldap' ) {
1145 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1146 $recref->{_password} = uc($1).$2;
1148 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1151 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1153 if ( $recref->{_password} =~
1154 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1155 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1158 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1161 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1164 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1166 #generate a password if it is blank
1167 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1168 unless length( $recref->{_password} );
1170 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1171 $recref->{_password} = $1;
1173 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1174 FS::Msgcat::_gettext('illegal_password_characters').
1175 ": ". $recref->{_password};
1178 if ( $password_noampersand ) {
1179 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1181 if ( $password_noexclamation ) {
1182 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1187 #carp "warning: _password_encoding unspecified\n";
1189 #generate a password if it is blank
1190 unless ( length( $recref->{_password} ) ) {
1192 $recref->{_password} =
1193 join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1194 $recref->{_password_encoding} = 'plain';
1198 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1199 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1200 $recref->{_password} = $1.$3;
1201 $recref->{_password_encoding} = 'plain';
1202 } elsif ( $recref->{_password} =~
1203 /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/
1205 $recref->{_password} = $1.$3;
1206 $recref->{_password_encoding} = 'crypt';
1207 } elsif ( $recref->{_password} eq '*' ) {
1208 $recref->{_password} = '*';
1209 $recref->{_password_encoding} = 'crypt';
1210 } elsif ( $recref->{_password} eq '!' ) {
1211 $recref->{_password_encoding} = 'crypt';
1212 $recref->{_password} = '!';
1213 } elsif ( $recref->{_password} eq '!!' ) {
1214 $recref->{_password} = '!!';
1215 $recref->{_password_encoding} = 'crypt';
1217 #return "Illegal password";
1218 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1219 FS::Msgcat::_gettext('illegal_password_characters').
1220 ": ". $recref->{_password};
1227 $self->SUPER::check;
1233 Internal function to check the username against the list of system usernames
1234 from the I<system_usernames> configuration value. Returns true if the username
1235 is listed on the system username list.
1241 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1242 $conf->config('system_usernames')
1246 =item _check_duplicate
1248 Internal method to check for duplicates usernames, username@domain pairs and
1251 If the I<global_unique-username> configuration value is set to B<username> or
1252 B<username@domain>, enforces global username or username@domain uniqueness.
1254 In all cases, check for duplicate uids and usernames or username@domain pairs
1255 per export and with identical I<svcpart> values.
1259 sub _check_duplicate {
1262 my $global_unique = $conf->config('global_unique-username') || 'none';
1263 return '' if $global_unique eq 'disabled';
1267 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1268 unless ( $part_svc ) {
1269 return 'unknown svcpart '. $self->svcpart;
1272 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1273 qsearch( 'svc_acct', { 'username' => $self->username } );
1274 return gettext('username_in_use')
1275 if $global_unique eq 'username' && @dup_user;
1277 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1278 qsearch( 'svc_acct', { 'username' => $self->username,
1279 'domsvc' => $self->domsvc } );
1280 return gettext('username_in_use')
1281 if $global_unique eq 'username@domain' && @dup_userdomain;
1284 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1285 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1286 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1287 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1292 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1293 my $exports = FS::part_export::export_info('svc_acct');
1294 my %conflict_user_svcpart;
1295 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1297 foreach my $part_export ( $part_svc->part_export ) {
1299 #this will catch to the same exact export
1300 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1302 #this will catch to exports w/same exporthost+type ???
1303 #my @other_part_export = qsearch('part_export', {
1304 # 'machine' => $part_export->machine,
1305 # 'exporttype' => $part_export->exporttype,
1307 #foreach my $other_part_export ( @other_part_export ) {
1308 # push @svcparts, map { $_->svcpart }
1309 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1312 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1313 #silly kludge to avoid uninitialized value errors
1314 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1315 ? $exports->{$part_export->exporttype}{'nodomain'}
1317 if ( $nodomain =~ /^Y/i ) {
1318 $conflict_user_svcpart{$_} = $part_export->exportnum
1321 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1326 foreach my $dup_user ( @dup_user ) {
1327 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1328 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1329 return "duplicate username ". $self->username.
1330 ": conflicts with svcnum ". $dup_user->svcnum.
1331 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1335 foreach my $dup_userdomain ( @dup_userdomain ) {
1336 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1337 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1338 return "duplicate username\@domain ". $self->email.
1339 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1340 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1344 foreach my $dup_uid ( @dup_uid ) {
1345 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1346 if ( exists($conflict_user_svcpart{$dup_svcpart})
1347 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1348 return "duplicate uid ". $self->uid.
1349 ": conflicts with svcnum ". $dup_uid->svcnum.
1351 ( $conflict_user_svcpart{$dup_svcpart}
1352 || $conflict_userdomain_svcpart{$dup_svcpart} );
1364 Depriciated, use radius_reply instead.
1369 carp "FS::svc_acct::radius depriciated, use radius_reply";
1370 $_[0]->radius_reply;
1375 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1376 reply attributes of this record.
1378 Note that this is now the preferred method for reading RADIUS attributes -
1379 accessing the columns directly is discouraged, as the column names are
1380 expected to change in the future.
1387 return %{ $self->{'radius_reply'} }
1388 if exists $self->{'radius_reply'};
1393 my($column, $attrib) = ($1, $2);
1394 #$attrib =~ s/_/\-/g;
1395 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1396 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1398 if ( $self->slipip && $self->slipip ne '0e0' ) {
1399 $reply{$radius_ip} = $self->slipip;
1402 if ( $self->seconds !~ /^$/ ) {
1403 $reply{'Session-Timeout'} = $self->seconds;
1411 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1412 check attributes of this record.
1414 Note that this is now the preferred method for reading RADIUS attributes -
1415 accessing the columns directly is discouraged, as the column names are
1416 expected to change in the future.
1423 return %{ $self->{'radius_check'} }
1424 if exists $self->{'radius_check'};
1429 my($column, $attrib) = ($1, $2);
1430 #$attrib =~ s/_/\-/g;
1431 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1432 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1435 my($pw_attrib, $password) = $self->radius_password;
1436 $check{$pw_attrib} = $password;
1438 my $cust_svc = $self->cust_svc;
1439 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1441 my $cust_pkg = $cust_svc->cust_pkg;
1442 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1443 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1450 =item radius_password
1452 Returns a key/value pair containing the RADIUS attribute name and value
1457 sub radius_password {
1460 my($pw_attrib, $password);
1461 if ( $self->_password_encoding eq 'ldap' ) {
1463 $pw_attrib = 'Password-With-Header';
1464 $password = $self->_password;
1466 } elsif ( $self->_password_encoding eq 'crypt' ) {
1468 $pw_attrib = 'Crypt-Password';
1469 $password = $self->_password;
1471 } elsif ( $self->_password_encoding eq 'plain' ) {
1473 $pw_attrib = $radius_password; #Cleartext-Password? man rlm_pap
1474 $password = $self->_password;
1478 $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
1479 $password = $self->_password;
1483 ($pw_attrib, $password);
1489 This method instructs the object to "snapshot" or freeze RADIUS check and
1490 reply attributes to the current values.
1494 #bah, my english is too broken this morning
1495 #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
1496 #the FS::cust_pkg's replace method to trigger the correct export updates when
1497 #package dates change)
1502 $self->{$_} = { $self->$_() }
1503 foreach qw( radius_reply radius_check );
1507 =item forget_snapshot
1509 This methos instructs the object to forget any previously snapshotted
1510 RADIUS check and reply attributes.
1514 sub forget_snapshot {
1518 foreach qw( radius_reply radius_check );
1522 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1524 Returns the domain associated with this account.
1526 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1533 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1534 my $svc_domain = $self->svc_domain(@_)
1535 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1536 $svc_domain->domain;
1541 Returns the FS::svc_domain record for this account's domain (see
1546 # FS::h_svc_acct has a history-aware svc_domain override
1551 ? $self->{'_domsvc'}
1552 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1557 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1561 #inherited from svc_Common
1563 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1565 Returns an email address associated with the account.
1567 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1574 $self->username. '@'. $self->domain(@_);
1579 Returns an array of FS::acct_snarf records associated with the account.
1580 If the acct_snarf table does not exist or there are no associated records,
1581 an empty list is returned
1587 return () unless dbdef->table('acct_snarf');
1588 eval "use FS::acct_snarf;";
1590 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1593 =item decrement_upbytes OCTETS
1595 Decrements the I<upbytes> field of this record by the given amount. If there
1596 is an error, returns the error, otherwise returns false.
1600 sub decrement_upbytes {
1601 shift->_op_usage('-', 'upbytes', @_);
1604 =item increment_upbytes OCTETS
1606 Increments the I<upbytes> field of this record by the given amount. If there
1607 is an error, returns the error, otherwise returns false.
1611 sub increment_upbytes {
1612 shift->_op_usage('+', 'upbytes', @_);
1615 =item decrement_downbytes OCTETS
1617 Decrements the I<downbytes> field of this record by the given amount. If there
1618 is an error, returns the error, otherwise returns false.
1622 sub decrement_downbytes {
1623 shift->_op_usage('-', 'downbytes', @_);
1626 =item increment_downbytes OCTETS
1628 Increments the I<downbytes> field of this record by the given amount. If there
1629 is an error, returns the error, otherwise returns false.
1633 sub increment_downbytes {
1634 shift->_op_usage('+', 'downbytes', @_);
1637 =item decrement_totalbytes OCTETS
1639 Decrements the I<totalbytes> field of this record by the given amount. If there
1640 is an error, returns the error, otherwise returns false.
1644 sub decrement_totalbytes {
1645 shift->_op_usage('-', 'totalbytes', @_);
1648 =item increment_totalbytes OCTETS
1650 Increments the I<totalbytes> field of this record by the given amount. If there
1651 is an error, returns the error, otherwise returns false.
1655 sub increment_totalbytes {
1656 shift->_op_usage('+', 'totalbytes', @_);
1659 =item decrement_seconds SECONDS
1661 Decrements the I<seconds> field of this record by the given amount. If there
1662 is an error, returns the error, otherwise returns false.
1666 sub decrement_seconds {
1667 shift->_op_usage('-', 'seconds', @_);
1670 =item increment_seconds SECONDS
1672 Increments the I<seconds> field of this record by the given amount. If there
1673 is an error, returns the error, otherwise returns false.
1677 sub increment_seconds {
1678 shift->_op_usage('+', 'seconds', @_);
1686 my %op2condition = (
1687 '-' => sub { my($self, $column, $amount) = @_;
1688 $self->$column - $amount <= 0;
1690 '+' => sub { my($self, $column, $amount) = @_;
1691 $self->$column + $amount > 0;
1694 my %op2warncondition = (
1695 '-' => sub { my($self, $column, $amount) = @_;
1696 my $threshold = $column . '_threshold';
1697 $self->$column - $amount <= $self->$threshold + 0;
1699 '+' => sub { my($self, $column, $amount) = @_;
1700 $self->$column + $amount > 0;
1705 my( $self, $op, $column, $amount ) = @_;
1707 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1708 ' ('. $self->email. "): $op $amount\n"
1711 return '' unless $amount;
1713 local $SIG{HUP} = 'IGNORE';
1714 local $SIG{INT} = 'IGNORE';
1715 local $SIG{QUIT} = 'IGNORE';
1716 local $SIG{TERM} = 'IGNORE';
1717 local $SIG{TSTP} = 'IGNORE';
1718 local $SIG{PIPE} = 'IGNORE';
1720 my $oldAutoCommit = $FS::UID::AutoCommit;
1721 local $FS::UID::AutoCommit = 0;
1724 my $sql = "UPDATE svc_acct SET $column = ".
1725 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1726 " $op ? WHERE svcnum = ?";
1730 my $sth = $dbh->prepare( $sql )
1731 or die "Error preparing $sql: ". $dbh->errstr;
1732 my $rv = $sth->execute($amount, $self->svcnum);
1733 die "Error executing $sql: ". $sth->errstr
1734 unless defined($rv);
1735 die "Can't update $column for svcnum". $self->svcnum
1738 my $action = $op2action{$op};
1740 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1741 ( $action eq 'suspend' && !$self->overlimit
1742 || $action eq 'unsuspend' && $self->overlimit )
1744 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1745 if ($part_export->option('overlimit_groups')) {
1747 my $other = new FS::svc_acct $self->hashref;
1748 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1749 ($self, $part_export->option('overlimit_groups'));
1750 $other->usergroup( $groups );
1751 if ($action eq 'suspend'){
1752 $new = $other; $old = $self;
1754 $new = $self; $old = $other;
1756 my $error = $part_export->export_replace($new, $old);
1757 $error ||= $self->overlimit($action);
1759 $dbh->rollback if $oldAutoCommit;
1760 return "Error replacing radius groups in export, ${op}: $error";
1766 if ( $conf->exists("svc_acct-usage_$action")
1767 && &{$op2condition{$op}}($self, $column, $amount) ) {
1768 #my $error = $self->$action();
1769 my $error = $self->cust_svc->cust_pkg->$action();
1770 # $error ||= $self->overlimit($action);
1772 $dbh->rollback if $oldAutoCommit;
1773 return "Error ${action}ing: $error";
1777 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1778 my $wqueue = new FS::queue {
1779 'svcnum' => $self->svcnum,
1780 'job' => 'FS::svc_acct::reached_threshold',
1785 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1789 my $error = $wqueue->insert(
1790 'svcnum' => $self->svcnum,
1792 'column' => $column,
1796 $dbh->rollback if $oldAutoCommit;
1797 return "Error queuing threshold activity: $error";
1801 warn "$me update successful; committing\n"
1803 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1809 my( $self, $valueref ) = @_;
1811 warn "$me set_usage called for svcnum ". $self->svcnum.
1812 ' ('. $self->email. "): ".
1813 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1816 local $SIG{HUP} = 'IGNORE';
1817 local $SIG{INT} = 'IGNORE';
1818 local $SIG{QUIT} = 'IGNORE';
1819 local $SIG{TERM} = 'IGNORE';
1820 local $SIG{TSTP} = 'IGNORE';
1821 local $SIG{PIPE} = 'IGNORE';
1823 local $FS::svc_Common::noexport_hack = 1;
1824 my $oldAutoCommit = $FS::UID::AutoCommit;
1825 local $FS::UID::AutoCommit = 0;
1830 foreach my $field (keys %$valueref){
1831 $reset = 1 if $valueref->{$field};
1832 $self->setfield($field, $valueref->{$field});
1833 $self->setfield( $field.'_threshold',
1834 int($self->getfield($field)
1835 * ( $conf->exists('svc_acct-usage_threshold')
1836 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1841 $handyhash{$field} = $self->getfield($field);
1842 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1844 #my $error = $self->replace; #NO! we avoid the call to ->check for
1845 #die $error if $error; #services not explicity changed via the UI
1847 my $sql = "UPDATE svc_acct SET " .
1848 join (',', map { "$_ = ?" } (keys %handyhash) ).
1849 " WHERE svcnum = ?";
1854 if (scalar(keys %handyhash)) {
1855 my $sth = $dbh->prepare( $sql )
1856 or die "Error preparing $sql: ". $dbh->errstr;
1857 my $rv = $sth->execute((values %handyhash), $self->svcnum);
1858 die "Error executing $sql: ". $sth->errstr
1859 unless defined($rv);
1860 die "Can't update usage for svcnum ". $self->svcnum
1867 if ($self->overlimit) {
1868 $error = $self->overlimit('unsuspend');
1869 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1870 if ($part_export->option('overlimit_groups')) {
1871 my $old = new FS::svc_acct $self->hashref;
1872 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1873 ($self, $part_export->option('overlimit_groups'));
1874 $old->usergroup( $groups );
1875 $error ||= $part_export->export_replace($self, $old);
1880 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1881 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1884 $dbh->rollback if $oldAutoCommit;
1885 return "Error unsuspending: $error";
1889 warn "$me update successful; committing\n"
1891 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1897 =item recharge HASHREF
1899 Increments usage columns by the amount specified in HASHREF as
1900 column=>amount pairs.
1905 my ($self, $vhash) = @_;
1908 warn "[$me] recharge called on $self: ". Dumper($self).
1909 "\nwith vhash: ". Dumper($vhash);
1912 my $oldAutoCommit = $FS::UID::AutoCommit;
1913 local $FS::UID::AutoCommit = 0;
1917 foreach my $column (keys %$vhash){
1918 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1922 $dbh->rollback if $oldAutoCommit;
1924 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1929 =item is_rechargeable
1931 Returns true if this svc_account can be "recharged" and false otherwise.
1935 sub is_rechargable {
1937 $self->seconds ne ''
1938 || $self->upbytes ne ''
1939 || $self->downbytes ne ''
1940 || $self->totalbytes ne '';
1943 =item seconds_since TIMESTAMP
1945 Returns the number of seconds this account has been online since TIMESTAMP,
1946 according to the session monitor (see L<FS::Session>).
1948 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1949 L<Time::Local> and L<Date::Parse> for conversion functions.
1953 #note: POD here, implementation in FS::cust_svc
1956 $self->cust_svc->seconds_since(@_);
1959 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1961 Returns the numbers of seconds this account has been online between
1962 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1963 external SQL radacct table, specified via sqlradius export. Sessions which
1964 started in the specified range but are still open are counted from session
1965 start to the end of the range (unless they are over 1 day old, in which case
1966 they are presumed missing their stop record and not counted). Also, sessions
1967 which end in the range but started earlier are counted from the start of the
1968 range to session end. Finally, sessions which start before the range but end
1969 after are counted for the entire range.
1971 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1972 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1977 #note: POD here, implementation in FS::cust_svc
1978 sub seconds_since_sqlradacct {
1980 $self->cust_svc->seconds_since_sqlradacct(@_);
1983 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1985 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1986 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1987 TIMESTAMP_END (exclusive).
1989 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1990 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1995 #note: POD here, implementation in FS::cust_svc
1996 sub attribute_since_sqlradacct {
1998 $self->cust_svc->attribute_since_sqlradacct(@_);
2001 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2003 Returns an array of hash references of this customers login history for the
2004 given time range. (document this better)
2008 sub get_session_history {
2010 $self->cust_svc->get_session_history(@_);
2013 =item last_login_text
2015 Returns text describing the time of last login.
2019 sub last_login_text {
2021 $self->last_login ? ctime($self->last_login) : 'unknown';
2024 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2029 my($self, $start, $end, %opt ) = @_;
2031 my $did = $self->username; #yup
2033 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2035 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2037 #SELECT $for_update * FROM cdr
2038 # WHERE calldate >= $start #need a conversion
2039 # AND calldate < $end #ditto
2040 # AND ( charged_party = "$did"
2041 # OR charged_party = "$prefix$did" #if length($prefix);
2042 # OR ( ( charged_party IS NULL OR charged_party = '' )
2044 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2047 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2050 if ( length($prefix) ) {
2052 " AND ( charged_party = '$did'
2053 OR charged_party = '$prefix$did'
2054 OR ( ( charged_party IS NULL OR charged_party = '' )
2056 ( src = '$did' OR src = '$prefix$did' )
2062 " AND ( charged_party = '$did'
2063 OR ( ( charged_party IS NULL OR charged_party = '' )
2073 'select' => "$for_update *",
2076 #( freesidestatus IS NULL OR freesidestatus = '' )
2077 'freesidestatus' => '',
2079 'extra_sql' => $charged_or_src,
2087 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2093 if ( $self->usergroup ) {
2094 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2095 unless ref($self->usergroup) eq 'ARRAY';
2096 #when provisioning records, export callback runs in svc_Common.pm before
2097 #radius_usergroup records can be inserted...
2098 @{$self->usergroup};
2100 map { $_->groupname }
2101 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2105 =item clone_suspended
2107 Constructor used by FS::part_export::_export_suspend fallback. Document
2112 sub clone_suspended {
2114 my %hash = $self->hash;
2115 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2116 new FS::svc_acct \%hash;
2119 =item clone_kludge_unsuspend
2121 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2126 sub clone_kludge_unsuspend {
2128 my %hash = $self->hash;
2129 $hash{_password} = '';
2130 new FS::svc_acct \%hash;
2133 =item check_password
2135 Checks the supplied password against the (possibly encrypted) password in the
2136 database. Returns true for a successful authentication, false for no match.
2138 Currently supported encryptions are: classic DES crypt() and MD5
2142 sub check_password {
2143 my($self, $check_password) = @_;
2145 #remove old-style SUSPENDED kludge, they should be allowed to login to
2146 #self-service and pay up
2147 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2149 if ( $self->_password_encoding eq 'ldap' ) {
2151 my $auth = from_rfc2307 Authen::Passphrase $self->_password;
2152 return $auth->match($check_password);
2154 } elsif ( $self->_password_encoding eq 'crypt' ) {
2156 my $auth = from_crypt Authen::Passphrase $self->_password;
2157 return $auth->match($check_password);
2159 } elsif ( $self->_password_encoding eq 'plain' ) {
2161 return $check_password eq $password;
2165 #XXX this could be replaced with Authen::Passphrase stuff
2167 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2169 } elsif ( length($password) < 13 ) { #plaintext
2170 $check_password eq $password;
2171 } elsif ( length($password) == 13 ) { #traditional DES crypt
2172 crypt($check_password, $password) eq $password;
2173 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2174 unix_md5_crypt($check_password, $password) eq $password;
2175 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2176 warn "Can't check password: Blowfish encryption not yet supported, ".
2177 "svcnum ". $self->svcnum. "\n";
2180 warn "Can't check password: Unrecognized encryption for svcnum ".
2181 $self->svcnum. "\n";
2189 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2191 Returns an encrypted password, either by passing through an encrypted password
2192 in the database or by encrypting a plaintext password from the database.
2194 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2195 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2196 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2197 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2198 encryption type is only used if the password is not already encrypted in the
2203 sub crypt_password {
2206 if ( $self->_password_encoding eq 'ldap' ) {
2208 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2211 #XXX this could be replaced with Authen::Passphrase stuff
2213 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2214 if ( $encryption eq 'crypt' ) {
2217 $saltset[int(rand(64))].$saltset[int(rand(64))]
2219 } elsif ( $encryption eq 'md5' ) {
2220 unix_md5_crypt( $self->_password );
2221 } elsif ( $encryption eq 'blowfish' ) {
2222 croak "unknown encryption method $encryption";
2224 croak "unknown encryption method $encryption";
2227 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2231 } elsif ( $self->_password_encoding eq 'crypt' ) {
2233 return $self->_password;
2235 } elsif ( $self->_password_encoding eq 'plain' ) {
2237 #XXX this could be replaced with Authen::Passphrase stuff
2239 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2240 if ( $encryption eq 'crypt' ) {
2243 $saltset[int(rand(64))].$saltset[int(rand(64))]
2245 } elsif ( $encryption eq 'md5' ) {
2246 unix_md5_crypt( $self->_password );
2247 } elsif ( $encryption eq 'blowfish' ) {
2248 croak "unknown encryption method $encryption";
2250 croak "unknown encryption method $encryption";
2255 if ( length($self->_password) == 13
2256 || $self->_password =~ /^\$(1|2a?)\$/
2257 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2263 #XXX this could be replaced with Authen::Passphrase stuff
2265 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2266 if ( $encryption eq 'crypt' ) {
2269 $saltset[int(rand(64))].$saltset[int(rand(64))]
2271 } elsif ( $encryption eq 'md5' ) {
2272 unix_md5_crypt( $self->_password );
2273 } elsif ( $encryption eq 'blowfish' ) {
2274 croak "unknown encryption method $encryption";
2276 croak "unknown encryption method $encryption";
2285 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2287 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2288 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2289 "{MD5}5426824942db4253f87a1009fd5d2d4".
2291 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2292 to work the same as the B</crypt_password> method.
2298 #eventually should check a "password-encoding" field
2300 if ( $self->_password_encoding eq 'ldap' ) {
2302 return $self->_password;
2304 } elsif ( $self->_password_encoding eq 'crypt' ) {
2306 if ( length($self->_password) == 13 ) { #crypt
2307 return '{CRYPT}'. $self->_password;
2308 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2310 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2311 # die "Blowfish encryption not supported in this context, svcnum ".
2312 # $self->svcnum. "\n";
2314 warn "encryption method not (yet?) supported in LDAP context";
2315 return '{CRYPT}*'; #unsupported, should not auth
2318 } elsif ( $self->_password_encoding eq 'plain' ) {
2320 return '{PLAIN}'. $self->_password;
2322 #return '{CLEARTEXT}'. $self->_password; #?
2326 if ( length($self->_password) == 13 ) { #crypt
2327 return '{CRYPT}'. $self->_password;
2328 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2330 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2331 warn "Blowfish encryption not supported in this context, svcnum ".
2332 $self->svcnum. "\n";
2335 #are these two necessary anymore?
2336 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2337 return '{SSHA}'. $1;
2338 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2339 return '{NS-MTA-MD5}'. $1;
2342 return '{PLAIN}'. $self->_password;
2344 #return '{CLEARTEXT}'. $self->_password; #?
2346 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2347 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2348 #if ( $encryption eq 'crypt' ) {
2349 # return '{CRYPT}'. crypt(
2351 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2353 #} elsif ( $encryption eq 'md5' ) {
2354 # unix_md5_crypt( $self->_password );
2355 #} elsif ( $encryption eq 'blowfish' ) {
2356 # croak "unknown encryption method $encryption";
2358 # croak "unknown encryption method $encryption";
2366 =item domain_slash_username
2368 Returns $domain/$username/
2372 sub domain_slash_username {
2374 $self->domain. '/'. $self->username. '/';
2377 =item virtual_maildir
2379 Returns $domain/maildirs/$username/
2383 sub virtual_maildir {
2385 $self->domain. '/maildirs/'. $self->username. '/';
2396 This is the FS::svc_acct job-queue-able version. It still uses
2397 FS::Misc::send_email under-the-hood.
2404 eval "use FS::Misc qw(send_email)";
2407 $opt{mimetype} ||= 'text/plain';
2408 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2410 my $error = send_email(
2411 'from' => $opt{from},
2413 'subject' => $opt{subject},
2414 'content-type' => $opt{mimetype},
2415 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2417 die $error if $error;
2420 =item check_and_rebuild_fuzzyfiles
2424 sub check_and_rebuild_fuzzyfiles {
2425 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2426 -e "$dir/svc_acct.username"
2427 or &rebuild_fuzzyfiles;
2430 =item rebuild_fuzzyfiles
2434 sub rebuild_fuzzyfiles {
2436 use Fcntl qw(:flock);
2438 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2442 open(USERNAMELOCK,">>$dir/svc_acct.username")
2443 or die "can't open $dir/svc_acct.username: $!";
2444 flock(USERNAMELOCK,LOCK_EX)
2445 or die "can't lock $dir/svc_acct.username: $!";
2447 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2449 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2450 or die "can't open $dir/svc_acct.username.tmp: $!";
2451 print USERNAMECACHE join("\n", @all_username), "\n";
2452 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2454 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2464 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2465 open(USERNAMECACHE,"<$dir/svc_acct.username")
2466 or die "can't open $dir/svc_acct.username: $!";
2467 my @array = map { chomp; $_; } <USERNAMECACHE>;
2468 close USERNAMECACHE;
2472 =item append_fuzzyfiles USERNAME
2476 sub append_fuzzyfiles {
2477 my $username = shift;
2479 &check_and_rebuild_fuzzyfiles;
2481 use Fcntl qw(:flock);
2483 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2485 open(USERNAME,">>$dir/svc_acct.username")
2486 or die "can't open $dir/svc_acct.username: $!";
2487 flock(USERNAME,LOCK_EX)
2488 or die "can't lock $dir/svc_acct.username: $!";
2490 print USERNAME "$username\n";
2492 flock(USERNAME,LOCK_UN)
2493 or die "can't unlock $dir/svc_acct.username: $!";
2501 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2505 sub radius_usergroup_selector {
2506 my $sel_groups = shift;
2507 my %sel_groups = map { $_=>1 } @$sel_groups;
2509 my $selectname = shift || 'radius_usergroup';
2512 my $sth = $dbh->prepare(
2513 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2514 ) or die $dbh->errstr;
2515 $sth->execute() or die $sth->errstr;
2516 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2520 function ${selectname}_doadd(object) {
2521 var myvalue = object.${selectname}_add.value;
2522 var optionName = new Option(myvalue,myvalue,false,true);
2523 var length = object.$selectname.length;
2524 object.$selectname.options[length] = optionName;
2525 object.${selectname}_add.value = "";
2528 <SELECT MULTIPLE NAME="$selectname">
2531 foreach my $group ( @all_groups ) {
2532 $html .= qq(<OPTION VALUE="$group");
2533 if ( $sel_groups{$group} ) {
2534 $html .= ' SELECTED';
2535 $sel_groups{$group} = 0;
2537 $html .= ">$group</OPTION>\n";
2539 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2540 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2542 $html .= '</SELECT>';
2544 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2545 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2550 =item reached_threshold
2552 Performs some activities when svc_acct thresholds (such as number of seconds
2553 remaining) are reached.
2557 sub reached_threshold {
2560 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2561 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2563 if ( $opt{'op'} eq '+' ){
2564 $svc_acct->setfield( $opt{'column'}.'_threshold',
2565 int($svc_acct->getfield($opt{'column'})
2566 * ( $conf->exists('svc_acct-usage_threshold')
2567 ? $conf->config('svc_acct-usage_threshold')/100
2572 my $error = $svc_acct->replace;
2573 die $error if $error;
2574 }elsif ( $opt{'op'} eq '-' ){
2576 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2577 return '' if ($threshold eq '' );
2579 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2580 my $error = $svc_acct->replace;
2581 die $error if $error; # email next time, i guess
2583 if ( $warning_template ) {
2584 eval "use FS::Misc qw(send_email)";
2587 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2588 my $cust_main = $cust_pkg->cust_main;
2590 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2591 $cust_main->invoicing_list,
2592 ($opt{'to'} ? $opt{'to'} : ())
2595 my $mimetype = $warning_mimetype;
2596 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2598 my $body = $warning_template->fill_in( HASH => {
2599 'custnum' => $cust_main->custnum,
2600 'username' => $svc_acct->username,
2601 'password' => $svc_acct->_password,
2602 'first' => $cust_main->first,
2603 'last' => $cust_main->getfield('last'),
2604 'pkg' => $cust_pkg->part_pkg->pkg,
2605 'column' => $opt{'column'},
2606 'amount' => $opt{'column'} =~/bytes/
2607 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2608 : $svc_acct->getfield($opt{'column'}),
2609 'threshold' => $opt{'column'} =~/bytes/
2610 ? FS::UI::bytecount::display_bytecount($threshold)
2615 my $error = send_email(
2616 'from' => $warning_from,
2618 'subject' => $warning_subject,
2619 'content-type' => $mimetype,
2620 'body' => [ map "$_\n", split("\n", $body) ],
2622 die $error if $error;
2625 die "unknown op: " . $opt{'op'};
2633 The $recref stuff in sub check should be cleaned up.
2635 The suspend, unsuspend and cancel methods update the database, but not the
2636 current object. This is probably a bug as it's unexpected and
2639 radius_usergroup_selector? putting web ui components in here? they should
2640 probably live somewhere else...
2642 insertion of RADIUS group stuff in insert could be done with child_objects now
2643 (would probably clean up export of them too)
2647 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2648 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2649 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2650 L<freeside-queued>), L<FS::svc_acct_pop>,
2651 schema.html from the base documentation.
2655 =item domain_select_hash %OPTIONS
2657 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2658 may at present purchase.
2660 Currently available options are: I<pkgnum> I<svcpart>
2664 sub domain_select_hash {
2665 my ($self, %options) = @_;
2671 $part_svc = $self->part_svc;
2672 $cust_pkg = $self->cust_svc->cust_pkg
2676 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2677 if $options{'svcpart'};
2679 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2680 if $options{'pkgnum'};
2682 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2683 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2684 %domains = map { $_->svcnum => $_->domain }
2685 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2686 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2687 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2688 %domains = map { $_->svcnum => $_->domain }
2689 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2690 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2691 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2693 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2696 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2697 my $svc_domain = qsearchs('svc_domain',
2698 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2699 if ( $svc_domain ) {
2700 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2702 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2703 $part_svc->part_svc_column('domsvc')->columnvalue;