4 use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
5 $dir_prefix @shells $usernamemin
6 $usernamemax $passwordmin $passwordmax
7 $username_ampersand $username_letter $username_letterfirst
8 $username_noperiod $username_nounderscore $username_nodash
9 $username_uppercase $username_percent $username_colon
10 $password_noampersand $password_noexclamation
11 $welcome_template $welcome_from
12 $welcome_subject $welcome_subject_template $welcome_mimetype
13 $warning_template $warning_from $warning_subject $warning_mimetype
16 $radius_password $radius_ip
23 use Crypt::PasswdMD5 1.2;
26 use FS::UID qw( datasrc driver_name );
28 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
29 use FS::Msgcat qw(gettext);
30 use FS::UI::bytecount;
36 use FS::cust_main_invoice;
40 use FS::radius_usergroup;
47 @ISA = qw( FS::svc_Common );
50 $me = '[FS::svc_acct]';
52 #ask FS::UID to run this stuff for us later
53 $FS::UID::callback{'FS::svc_acct'} = sub {
55 $dir_prefix = $conf->config('home');
56 @shells = $conf->config('shells');
57 $usernamemin = $conf->config('usernamemin') || 2;
58 $usernamemax = $conf->config('usernamemax');
59 $passwordmin = $conf->config('passwordmin') || 6;
60 $passwordmax = $conf->config('passwordmax') || 8;
61 $username_letter = $conf->exists('username-letter');
62 $username_letterfirst = $conf->exists('username-letterfirst');
63 $username_noperiod = $conf->exists('username-noperiod');
64 $username_nounderscore = $conf->exists('username-nounderscore');
65 $username_nodash = $conf->exists('username-nodash');
66 $username_uppercase = $conf->exists('username-uppercase');
67 $username_ampersand = $conf->exists('username-ampersand');
68 $username_percent = $conf->exists('username-percent');
69 $username_colon = $conf->exists('username-colon');
70 $password_noampersand = $conf->exists('password-noexclamation');
71 $password_noexclamation = $conf->exists('password-noexclamation');
72 $dirhash = $conf->config('dirhash') || 0;
73 if ( $conf->exists('welcome_email') ) {
74 $welcome_template = new Text::Template (
76 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
77 ) or warn "can't create welcome email template: $Text::Template::ERROR";
78 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
79 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
80 $welcome_subject_template = new Text::Template (
82 SOURCE => $welcome_subject,
83 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
84 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
86 $welcome_template = '';
88 $welcome_subject = '';
89 $welcome_mimetype = '';
91 if ( $conf->exists('warning_email') ) {
92 $warning_template = new Text::Template (
94 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
95 ) or warn "can't create warning email template: $Text::Template::ERROR";
96 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
97 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
98 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
99 $warning_cc = $conf->config('warning_email-cc');
101 $warning_template = '';
103 $warning_subject = '';
104 $warning_mimetype = '';
107 $smtpmachine = $conf->config('smtpmachine');
108 $radius_password = $conf->config('radius-password') || 'Password';
109 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
110 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
113 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
114 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
118 my ( $hashref, $cache ) = @_;
119 if ( $hashref->{'svc_acct_svcnum'} ) {
120 $self->{'_domsvc'} = FS::svc_domain->new( {
121 'svcnum' => $hashref->{'domsvc'},
122 'domain' => $hashref->{'svc_acct_domain'},
123 'catchall' => $hashref->{'svc_acct_catchall'},
130 FS::svc_acct - Object methods for svc_acct records
136 $record = new FS::svc_acct \%hash;
137 $record = new FS::svc_acct { 'column' => 'value' };
139 $error = $record->insert;
141 $error = $new_record->replace($old_record);
143 $error = $record->delete;
145 $error = $record->check;
147 $error = $record->suspend;
149 $error = $record->unsuspend;
151 $error = $record->cancel;
153 %hash = $record->radius;
155 %hash = $record->radius_reply;
157 %hash = $record->radius_check;
159 $domain = $record->domain;
161 $svc_domain = $record->svc_domain;
163 $email = $record->email;
165 $seconds_since = $record->seconds_since($timestamp);
169 An FS::svc_acct object represents an account. FS::svc_acct inherits from
170 FS::svc_Common. The following fields are currently supported:
174 =item svcnum - primary key (assigned automatcially for new accounts)
178 =item _password - generated if blank
180 =item sec_phrase - security phrase
182 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
190 =item dir - set automatically if blank (and uid is not)
194 =item quota - (unimplementd)
196 =item slipip - IP address
206 =item domsvc - svcnum from svc_domain
208 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
210 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
220 Creates a new account. To add the account to the database, see L<"insert">.
227 'longname_plural' => 'Access accounts and mailboxes',
228 'sorts' => [ 'username', 'uid', 'last_login', ],
229 'display_weight' => 10,
230 'cancel_weight' => 50,
232 'dir' => 'Home directory',
235 def_label => 'UID (set to fixed and blank for no UIDs)',
238 'slipip' => 'IP address',
239 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
241 label => 'Access number',
243 select_table => 'svc_acct_pop',
244 select_key => 'popnum',
245 select_label => 'city',
251 disable_default => 1,
258 disable_inventory => 1,
261 '_password' => 'Password',
264 def_label => 'GID (when blank, defaults to UID)',
268 #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)',
270 def_label=> 'Shell (set to blank for no shell tracking)',
272 select_list => [ $conf->config('shells') ],
273 disable_inventory => 1,
276 'finger' => 'Real name', # (GECOS)',
279 #def_label => 'svcnum from svc_domain',
281 select_table => 'svc_domain',
282 select_key => 'svcnum',
283 select_label => 'domain',
284 disable_inventory => 1,
288 label => 'RADIUS groups',
289 type => 'radius_usergroup_selector',
290 disable_inventory => 1,
293 'seconds' => { label => 'Seconds',
295 disable_inventory => 1,
297 disable_part_svc_column => 1,
299 'upbytes' => { label => 'Upload',
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 'downbytes' => { label => 'Download',
309 disable_inventory => 1,
311 'format' => \&FS::UI::bytecount::display_bytecount,
312 'parse' => \&FS::UI::bytecount::parse_bytecount,
313 disable_part_svc_column => 1,
315 'totalbytes'=> { label => 'Total up and download',
317 disable_inventory => 1,
319 'format' => \&FS::UI::bytecount::display_bytecount,
320 'parse' => \&FS::UI::bytecount::parse_bytecount,
321 disable_part_svc_column => 1,
323 'seconds_threshold' => { label => 'Seconds threshold',
325 disable_inventory => 1,
327 disable_part_svc_column => 1,
329 'upbytes_threshold' => { label => 'Upload 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,
337 'downbytes_threshold' => { label => 'Download threshold',
339 disable_inventory => 1,
341 'format' => \&FS::UI::bytecount::display_bytecount,
342 'parse' => \&FS::UI::bytecount::parse_bytecount,
343 disable_part_svc_column => 1,
345 'totalbytes_threshold'=> { label => 'Total up and download threshold',
347 disable_inventory => 1,
349 'format' => \&FS::UI::bytecount::display_bytecount,
350 'parse' => \&FS::UI::bytecount::parse_bytecount,
351 disable_part_svc_column => 1,
354 label => 'Last login',
358 label => 'Last logout',
365 sub table { 'svc_acct'; }
369 #false laziness with edit/svc_acct.cgi
371 my( $self, $groups ) = @_;
372 if ( ref($groups) eq 'ARRAY' ) {
374 } elsif ( length($groups) ) {
375 [ split(/\s*,\s*/, $groups) ];
384 shift->_lastlog('in', @_);
388 shift->_lastlog('out', @_);
392 my( $self, $op, $time ) = @_;
394 if ( defined($time) ) {
395 warn "$me last_log$op called on svcnum ". $self->svcnum.
396 ' ('. $self->email. "): $time\n"
401 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
405 my $sth = $dbh->prepare( $sql )
406 or die "Error preparing $sql: ". $dbh->errstr;
407 my $rv = $sth->execute($time, $self->svcnum);
408 die "Error executing $sql: ". $sth->errstr
410 die "Can't update last_log$op for svcnum". $self->svcnum
413 $self->{'Hash'}->{"last_log$op"} = $time;
415 $self->getfield("last_log$op");
419 =item search_sql STRING
421 Class method which returns an SQL fragment to search for the given string.
426 my( $class, $string ) = @_;
427 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
428 my( $username, $domain ) = ( $1, $2 );
429 my $q_username = dbh->quote($username);
430 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
432 "svc_acct.username = $q_username AND ( ".
433 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
438 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
440 $class->search_sql_field('slipip', $string ).
442 $class->search_sql_field('username', $string ).
445 $class->search_sql_field('username', $string);
449 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
451 Returns the "username@domain" string for this account.
453 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
463 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
465 Returns a longer string label for this acccount ("Real Name <username@domain>"
466 if available, or "username@domain").
468 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
475 my $label = $self->label(@_);
476 my $finger = $self->finger;
477 return $label unless $finger =~ /\S/;
478 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
479 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
483 =item insert [ , OPTION => VALUE ... ]
485 Adds this account to the database. If there is an error, returns the error,
486 otherwise returns false.
488 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
489 defined. An FS::cust_svc record will be created and inserted.
491 The additional field I<usergroup> can optionally be defined; if so it should
492 contain an arrayref of group names. See L<FS::radius_usergroup>.
494 The additional field I<child_objects> can optionally be defined; if so it
495 should contain an arrayref of FS::tablename objects. They will have their
496 svcnum fields set and will be inserted after this record, but before any
497 exports are run. Each element of the array can also optionally be a
498 two-element array reference containing the child object and the name of an
499 alternate field to be filled in with the newly-inserted svcnum, for example
500 C<[ $svc_forward, 'srcsvc' ]>
502 Currently available options are: I<depend_jobnum>
504 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
505 jobnums), all provisioning jobs will have a dependancy on the supplied
506 jobnum(s) (they will not run until the specific job(s) complete(s)).
508 (TODOC: L<FS::queue> and L<freeside-queued>)
510 (TODOC: new exports!)
519 warn "[$me] insert called on $self: ". Dumper($self).
520 "\nwith options: ". Dumper(%options);
523 local $SIG{HUP} = 'IGNORE';
524 local $SIG{INT} = 'IGNORE';
525 local $SIG{QUIT} = 'IGNORE';
526 local $SIG{TERM} = 'IGNORE';
527 local $SIG{TSTP} = 'IGNORE';
528 local $SIG{PIPE} = 'IGNORE';
530 my $oldAutoCommit = $FS::UID::AutoCommit;
531 local $FS::UID::AutoCommit = 0;
534 my $error = $self->check;
535 return $error if $error;
537 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
538 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
539 unless ( $cust_svc ) {
540 $dbh->rollback if $oldAutoCommit;
541 return "no cust_svc record found for svcnum ". $self->svcnum;
543 $self->pkgnum($cust_svc->pkgnum);
544 $self->svcpart($cust_svc->svcpart);
547 $error = $self->_check_duplicate;
549 $dbh->rollback if $oldAutoCommit;
553 # set usage fields and thresholds if unset but set in a package def
554 if ( $self->pkgnum ) {
555 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
556 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
557 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
559 my %values = $part_pkg->usage_valuehash;
560 my $multiplier = $conf->exists('svc_acct-usage_threshold')
561 ? 1 - $conf->config('svc_acct-usage_threshold')/100
562 : 0.20; #doesn't matter
564 foreach ( keys %values ) {
565 next if $self->getfield($_);
566 $self->setfield( $_, $values{$_} );
567 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
568 if $conf->exists('svc_acct-usage_threshold');
575 $error = $self->SUPER::insert(
576 'jobnums' => \@jobnums,
577 'child_objects' => $self->child_objects,
581 $dbh->rollback if $oldAutoCommit;
585 if ( $self->usergroup ) {
586 foreach my $groupname ( @{$self->usergroup} ) {
587 my $radius_usergroup = new FS::radius_usergroup ( {
588 svcnum => $self->svcnum,
589 groupname => $groupname,
591 my $error = $radius_usergroup->insert;
593 $dbh->rollback if $oldAutoCommit;
599 unless ( $skip_fuzzyfiles ) {
600 $error = $self->queue_fuzzyfiles_update;
602 $dbh->rollback if $oldAutoCommit;
603 return "updating fuzzy search cache: $error";
607 my $cust_pkg = $self->cust_svc->cust_pkg;
610 my $cust_main = $cust_pkg->cust_main;
612 if ( $conf->exists('emailinvoiceautoalways')
613 || $conf->exists('emailinvoiceauto')
614 && ! $cust_main->invoicing_list_emailonly
616 my @invoicing_list = $cust_main->invoicing_list;
617 push @invoicing_list, $self->email;
618 $cust_main->invoicing_list(\@invoicing_list);
623 if ( $welcome_template && $cust_pkg ) {
624 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
628 'custnum' => $self->custnum,
629 'username' => $self->username,
630 'password' => $self->_password,
631 'first' => $cust_main->first,
632 'last' => $cust_main->getfield('last'),
633 'pkg' => $cust_pkg->part_pkg->pkg,
635 my $wqueue = new FS::queue {
636 'svcnum' => $self->svcnum,
637 'job' => 'FS::svc_acct::send_email'
639 my $error = $wqueue->insert(
641 'from' => $welcome_from,
642 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
643 'mimetype' => $welcome_mimetype,
644 'body' => $welcome_template->fill_in( HASH => \%hash, ),
647 $dbh->rollback if $oldAutoCommit;
648 return "error queuing welcome email: $error";
651 if ( $options{'depend_jobnum'} ) {
652 warn "$me depend_jobnum found; adding to welcome email dependancies"
654 if ( ref($options{'depend_jobnum'}) ) {
655 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
656 "to welcome email dependancies"
658 push @jobnums, @{ $options{'depend_jobnum'} };
660 warn "$me adding job $options{'depend_jobnum'} ".
661 "to welcome email dependancies"
663 push @jobnums, $options{'depend_jobnum'};
667 foreach my $jobnum ( @jobnums ) {
668 my $error = $wqueue->depend_insert($jobnum);
670 $dbh->rollback if $oldAutoCommit;
671 return "error queuing welcome email job dependancy: $error";
681 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
687 Deletes this account from the database. If there is an error, returns the
688 error, otherwise returns false.
690 The corresponding FS::cust_svc record will be deleted as well.
692 (TODOC: new exports!)
699 return "can't delete system account" if $self->_check_system;
701 return "Can't delete an account which is a (svc_forward) source!"
702 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
704 return "Can't delete an account which is a (svc_forward) destination!"
705 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
707 return "Can't delete an account with (svc_www) web service!"
708 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
710 # what about records in session ? (they should refer to history table)
712 local $SIG{HUP} = 'IGNORE';
713 local $SIG{INT} = 'IGNORE';
714 local $SIG{QUIT} = 'IGNORE';
715 local $SIG{TERM} = 'IGNORE';
716 local $SIG{TSTP} = 'IGNORE';
717 local $SIG{PIPE} = 'IGNORE';
719 my $oldAutoCommit = $FS::UID::AutoCommit;
720 local $FS::UID::AutoCommit = 0;
723 foreach my $cust_main_invoice (
724 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
726 unless ( defined($cust_main_invoice) ) {
727 warn "WARNING: something's wrong with qsearch";
730 my %hash = $cust_main_invoice->hash;
731 $hash{'dest'} = $self->email;
732 my $new = new FS::cust_main_invoice \%hash;
733 my $error = $new->replace($cust_main_invoice);
735 $dbh->rollback if $oldAutoCommit;
740 foreach my $svc_domain (
741 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
743 my %hash = new FS::svc_domain->hash;
744 $hash{'catchall'} = '';
745 my $new = new FS::svc_domain \%hash;
746 my $error = $new->replace($svc_domain);
748 $dbh->rollback if $oldAutoCommit;
753 my $error = $self->SUPER::delete;
755 $dbh->rollback if $oldAutoCommit;
759 foreach my $radius_usergroup (
760 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
762 my $error = $radius_usergroup->delete;
764 $dbh->rollback if $oldAutoCommit;
769 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
773 =item replace OLD_RECORD
775 Replaces OLD_RECORD with this one in the database. If there is an error,
776 returns the error, otherwise returns false.
778 The additional field I<usergroup> can optionally be defined; if so it should
779 contain an arrayref of group names. See L<FS::radius_usergroup>.
785 my ( $new, $old ) = ( shift, shift );
787 warn "$me replacing $old with $new\n" if $DEBUG;
789 # We absolutely have to have an old vs. new record to make this work.
790 if (!defined($old)) {
791 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
794 return "can't modify system account" if $old->_check_system;
797 #no warnings 'numeric'; #alas, a 5.006-ism
800 foreach my $xid (qw( uid gid )) {
802 return "Can't change $xid!"
803 if ! $conf->exists("svc_acct-edit_$xid")
804 && $old->$xid() != $new->$xid()
805 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
810 #change homdir when we change username
811 $new->setfield('dir', '') if $old->username ne $new->username;
813 local $SIG{HUP} = 'IGNORE';
814 local $SIG{INT} = 'IGNORE';
815 local $SIG{QUIT} = 'IGNORE';
816 local $SIG{TERM} = 'IGNORE';
817 local $SIG{TSTP} = 'IGNORE';
818 local $SIG{PIPE} = 'IGNORE';
820 my $oldAutoCommit = $FS::UID::AutoCommit;
821 local $FS::UID::AutoCommit = 0;
824 # redundant, but so $new->usergroup gets set
825 $error = $new->check;
826 return $error if $error;
828 $old->usergroup( [ $old->radius_groups ] );
830 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
831 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
833 if ( $new->usergroup ) {
834 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
835 my @newgroups = @{$new->usergroup};
836 foreach my $oldgroup ( @{$old->usergroup} ) {
837 if ( grep { $oldgroup eq $_ } @newgroups ) {
838 @newgroups = grep { $oldgroup ne $_ } @newgroups;
841 my $radius_usergroup = qsearchs('radius_usergroup', {
842 svcnum => $old->svcnum,
843 groupname => $oldgroup,
845 my $error = $radius_usergroup->delete;
847 $dbh->rollback if $oldAutoCommit;
848 return "error deleting radius_usergroup $oldgroup: $error";
852 foreach my $newgroup ( @newgroups ) {
853 my $radius_usergroup = new FS::radius_usergroup ( {
854 svcnum => $new->svcnum,
855 groupname => $newgroup,
857 my $error = $radius_usergroup->insert;
859 $dbh->rollback if $oldAutoCommit;
860 return "error adding radius_usergroup $newgroup: $error";
866 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
867 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
868 $error = $new->_check_duplicate;
870 $dbh->rollback if $oldAutoCommit;
875 $error = $new->SUPER::replace($old);
877 $dbh->rollback if $oldAutoCommit;
878 return $error if $error;
881 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
882 $error = $new->queue_fuzzyfiles_update;
884 $dbh->rollback if $oldAutoCommit;
885 return "updating fuzzy search cache: $error";
889 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
893 =item queue_fuzzyfiles_update
895 Used by insert & replace to update the fuzzy search cache
899 sub queue_fuzzyfiles_update {
902 local $SIG{HUP} = 'IGNORE';
903 local $SIG{INT} = 'IGNORE';
904 local $SIG{QUIT} = 'IGNORE';
905 local $SIG{TERM} = 'IGNORE';
906 local $SIG{TSTP} = 'IGNORE';
907 local $SIG{PIPE} = 'IGNORE';
909 my $oldAutoCommit = $FS::UID::AutoCommit;
910 local $FS::UID::AutoCommit = 0;
913 my $queue = new FS::queue {
914 'svcnum' => $self->svcnum,
915 'job' => 'FS::svc_acct::append_fuzzyfiles'
917 my $error = $queue->insert($self->username);
919 $dbh->rollback if $oldAutoCommit;
920 return "queueing job (transaction rolled back): $error";
923 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
931 Suspends this account by calling export-specific suspend hooks. If there is
932 an error, returns the error, otherwise returns false.
934 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
940 return "can't suspend system account" if $self->_check_system;
941 $self->SUPER::suspend;
946 Unsuspends this account by by calling export-specific suspend hooks. If there
947 is an error, returns the error, otherwise returns false.
949 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
955 my %hash = $self->hash;
956 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
957 $hash{_password} = $1;
958 my $new = new FS::svc_acct ( \%hash );
959 my $error = $new->replace($self);
960 return $error if $error;
963 $self->SUPER::unsuspend;
968 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
970 If the B<auto_unset_catchall> configuration option is set, this method will
971 automatically remove any references to the canceled service in the catchall
972 field of svc_domain. This allows packages that contain both a svc_domain and
973 its catchall svc_acct to be canceled in one step.
978 # Only one thing to do at this level
980 foreach my $svc_domain (
981 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
982 if($conf->exists('auto_unset_catchall')) {
983 my %hash = $svc_domain->hash;
984 $hash{catchall} = '';
985 my $new = new FS::svc_domain ( \%hash );
986 my $error = $new->replace($svc_domain);
987 return $error if $error;
989 return "cannot unprovision svc_acct #".$self->svcnum.
990 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
994 $self->SUPER::cancel;
1000 Checks all fields to make sure this is a valid service. If there is an error,
1001 returns the error, otherwise returns false. Called by the insert and replace
1004 Sets any fixed values; see L<FS::part_svc>.
1011 my($recref) = $self->hashref;
1013 my $x = $self->setfixed( $self->_fieldhandlers );
1014 return $x unless ref($x);
1017 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1019 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1022 my $error = $self->ut_numbern('svcnum')
1023 #|| $self->ut_number('domsvc')
1024 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1025 || $self->ut_textn('sec_phrase')
1026 || $self->ut_snumbern('seconds')
1027 || $self->ut_snumbern('upbytes')
1028 || $self->ut_snumbern('downbytes')
1029 || $self->ut_snumbern('totalbytes')
1031 return $error if $error;
1033 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1034 if ( $username_uppercase ) {
1035 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1036 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1037 $recref->{username} = $1;
1039 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1040 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1041 $recref->{username} = $1;
1044 if ( $username_letterfirst ) {
1045 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1046 } elsif ( $username_letter ) {
1047 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1049 if ( $username_noperiod ) {
1050 $recref->{username} =~ /\./ and return gettext('illegal_username');
1052 if ( $username_nounderscore ) {
1053 $recref->{username} =~ /_/ and return gettext('illegal_username');
1055 if ( $username_nodash ) {
1056 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1058 unless ( $username_ampersand ) {
1059 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1061 if ( $password_noampersand ) {
1062 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1064 if ( $password_noexclamation ) {
1065 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1067 unless ( $username_percent ) {
1068 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1070 unless ( $username_colon ) {
1071 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1074 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1075 $recref->{popnum} = $1;
1076 return "Unknown popnum" unless
1077 ! $recref->{popnum} ||
1078 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1080 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1082 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1083 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1085 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1086 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1087 #not all systems use gid=uid
1088 #you can set a fixed gid in part_svc
1090 return "Only root can have uid 0"
1091 if $recref->{uid} == 0
1092 && $recref->{username} !~ /^(root|toor|smtp)$/;
1094 unless ( $recref->{username} eq 'sync' ) {
1095 if ( grep $_ eq $recref->{shell}, @shells ) {
1096 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1098 return "Illegal shell \`". $self->shell. "\'; ".
1099 $conf->dir. "/shells contains: @shells";
1102 $recref->{shell} = '/bin/sync';
1106 $recref->{gid} ne '' ?
1107 return "Can't have gid without uid" : ( $recref->{gid}='' );
1108 #$recref->{dir} ne '' ?
1109 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1110 $recref->{shell} ne '' ?
1111 return "Can't have shell without uid" : ( $recref->{shell}='' );
1114 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1116 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1117 or return "Illegal directory: ". $recref->{dir};
1118 $recref->{dir} = $1;
1119 return "Illegal directory"
1120 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1121 return "Illegal directory"
1122 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1123 unless ( $recref->{dir} ) {
1124 $recref->{dir} = $dir_prefix . '/';
1125 if ( $dirhash > 0 ) {
1126 for my $h ( 1 .. $dirhash ) {
1127 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1129 } elsif ( $dirhash < 0 ) {
1130 for my $h ( reverse $dirhash .. -1 ) {
1131 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1134 $recref->{dir} .= $recref->{username};
1140 # $error = $self->ut_textn('finger');
1141 # return $error if $error;
1142 if ( $self->getfield('finger') eq '' ) {
1143 my $cust_pkg = $self->svcnum
1144 ? $self->cust_svc->cust_pkg
1145 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1147 my $cust_main = $cust_pkg->cust_main;
1148 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1151 $self->getfield('finger') =~
1152 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1153 or return "Illegal finger: ". $self->getfield('finger');
1154 $self->setfield('finger', $1);
1156 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1157 $recref->{quota} = $1;
1159 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1160 if ( $recref->{slipip} eq '' ) {
1161 $recref->{slipip} = '';
1162 } elsif ( $recref->{slipip} eq '0e0' ) {
1163 $recref->{slipip} = '0e0';
1165 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1166 or return "Illegal slipip: ". $self->slipip;
1167 $recref->{slipip} = $1;
1172 #arbitrary RADIUS stuff; allow ut_textn for now
1173 foreach ( grep /^radius_/, fields('svc_acct') ) {
1174 $self->ut_textn($_);
1177 #generate a password if it is blank
1178 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1179 unless ( $recref->{_password} );
1181 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1182 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1183 $recref->{_password} = $1.$3;
1184 #uncomment this to encrypt password immediately upon entry, or run
1185 #bin/crypt_pw in cron to give new users a window during which their
1186 #password is available to techs, for faxing, etc. (also be aware of
1188 #$recref->{password} = $1.
1189 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1191 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1192 $recref->{_password} = $1.$3;
1193 } elsif ( $recref->{_password} eq '*' ) {
1194 $recref->{_password} = '*';
1195 } elsif ( $recref->{_password} eq '!' ) {
1196 $recref->{_password} = '!';
1197 } elsif ( $recref->{_password} eq '!!' ) {
1198 $recref->{_password} = '!!';
1200 #return "Illegal password";
1201 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1202 FS::Msgcat::_gettext('illegal_password_characters').
1203 ": ". $recref->{_password};
1206 $self->SUPER::check;
1211 Internal function to check the username against the list of system usernames
1212 from the I<system_usernames> configuration value. Returns true if the username
1213 is listed on the system username list.
1219 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1220 $conf->config('system_usernames')
1224 =item _check_duplicate
1226 Internal function to check for duplicates usernames, username@domain pairs and
1229 If the I<global_unique-username> configuration value is set to B<username> or
1230 B<username@domain>, enforces global username or username@domain uniqueness.
1232 In all cases, check for duplicate uids and usernames or username@domain pairs
1233 per export and with identical I<svcpart> values.
1237 sub _check_duplicate {
1240 my $global_unique = $conf->config('global_unique-username') || 'none';
1241 return '' if $global_unique eq 'disabled';
1243 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1244 if ( driver_name =~ /^Pg/i ) {
1245 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1247 } elsif ( driver_name =~ /^mysql/i ) {
1248 dbh->do("SELECT * FROM duplicate_lock
1249 WHERE lockname = 'svc_acct'
1251 ) or die dbh->errstr;
1253 die "unknown database ". driver_name.
1254 "; don't know how to lock for duplicate search";
1256 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1258 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1259 unless ( $part_svc ) {
1260 return 'unknown svcpart '. $self->svcpart;
1263 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1264 qsearch( 'svc_acct', { 'username' => $self->username } );
1265 return gettext('username_in_use')
1266 if $global_unique eq 'username' && @dup_user;
1268 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1269 qsearch( 'svc_acct', { 'username' => $self->username,
1270 'domsvc' => $self->domsvc } );
1271 return gettext('username_in_use')
1272 if $global_unique eq 'username@domain' && @dup_userdomain;
1275 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1276 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1277 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1278 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1283 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1284 my $exports = FS::part_export::export_info('svc_acct');
1285 my %conflict_user_svcpart;
1286 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1288 foreach my $part_export ( $part_svc->part_export ) {
1290 #this will catch to the same exact export
1291 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1293 #this will catch to exports w/same exporthost+type ???
1294 #my @other_part_export = qsearch('part_export', {
1295 # 'machine' => $part_export->machine,
1296 # 'exporttype' => $part_export->exporttype,
1298 #foreach my $other_part_export ( @other_part_export ) {
1299 # push @svcparts, map { $_->svcpart }
1300 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1303 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1304 #silly kludge to avoid uninitialized value errors
1305 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1306 ? $exports->{$part_export->exporttype}{'nodomain'}
1308 if ( $nodomain =~ /^Y/i ) {
1309 $conflict_user_svcpart{$_} = $part_export->exportnum
1312 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1317 foreach my $dup_user ( @dup_user ) {
1318 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1319 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1320 return "duplicate username ". $self->username.
1321 ": conflicts with svcnum ". $dup_user->svcnum.
1322 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1326 foreach my $dup_userdomain ( @dup_userdomain ) {
1327 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1328 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1329 return "duplicate username\@domain ". $self->email.
1330 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1331 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1335 foreach my $dup_uid ( @dup_uid ) {
1336 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1337 if ( exists($conflict_user_svcpart{$dup_svcpart})
1338 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1339 return "duplicate uid ". $self->uid.
1340 ": conflicts with svcnum ". $dup_uid->svcnum.
1342 ( $conflict_user_svcpart{$dup_svcpart}
1343 || $conflict_userdomain_svcpart{$dup_svcpart} );
1355 Depriciated, use radius_reply instead.
1360 carp "FS::svc_acct::radius depriciated, use radius_reply";
1361 $_[0]->radius_reply;
1366 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1367 reply attributes of this record.
1369 Note that this is now the preferred method for reading RADIUS attributes -
1370 accessing the columns directly is discouraged, as the column names are
1371 expected to change in the future.
1378 return %{ $self->{'radius_reply'} }
1379 if exists $self->{'radius_reply'};
1384 my($column, $attrib) = ($1, $2);
1385 #$attrib =~ s/_/\-/g;
1386 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1387 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1389 if ( $self->slipip && $self->slipip ne '0e0' ) {
1390 $reply{$radius_ip} = $self->slipip;
1393 if ( $self->seconds !~ /^$/ ) {
1394 $reply{'Session-Timeout'} = $self->seconds;
1397 if ( $conf->exists('radius-chillispot-max') ) {
1398 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1400 #hmm. just because sqlradius.pm says so?
1407 foreach my $what (qw( input output total )) {
1408 my $is = $whatis{$what}.'bytes';
1409 if ( $self->$is() =~ /\d/ ) {
1410 my $big = new Math::BigInt $self->$is();
1411 $big = new Math::BigInto '0' if $big->is_neg();
1412 my $att = "Chillispot-Max-\u$what";
1413 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1414 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1425 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1426 check attributes of this record.
1428 Note that this is now the preferred method for reading RADIUS attributes -
1429 accessing the columns directly is discouraged, as the column names are
1430 expected to change in the future.
1437 return %{ $self->{'radius_check'} }
1438 if exists $self->{'radius_check'};
1443 my($column, $attrib) = ($1, $2);
1444 #$attrib =~ s/_/\-/g;
1445 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1446 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1448 my $password = $self->_password;
1449 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1451 my $cust_svc = $self->cust_svc;
1453 my $cust_pkg = $cust_svc->cust_pkg;
1454 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1455 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1458 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1459 "; can't set Expiration\n"
1469 This method instructs the object to "snapshot" or freeze RADIUS check and
1470 reply attributes to the current values.
1474 #bah, my english is too broken this morning
1475 #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
1476 #the FS::cust_pkg's replace method to trigger the correct export updates when
1477 #package dates change)
1482 $self->{$_} = { $self->$_() }
1483 foreach qw( radius_reply radius_check );
1487 =item forget_snapshot
1489 This methos instructs the object to forget any previously snapshotted
1490 RADIUS check and reply attributes.
1494 sub forget_snapshot {
1498 foreach qw( radius_reply radius_check );
1502 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1504 Returns the domain associated with this account.
1506 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1513 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1514 my $svc_domain = $self->svc_domain(@_)
1515 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1516 $svc_domain->domain;
1521 Returns the FS::svc_domain record for this account's domain (see
1526 # FS::h_svc_acct has a history-aware svc_domain override
1531 ? $self->{'_domsvc'}
1532 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1537 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1541 #inherited from svc_Common
1543 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1545 Returns an email address associated with the account.
1547 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1554 $self->username. '@'. $self->domain(@_);
1559 Returns an array of FS::acct_snarf records associated with the account.
1560 If the acct_snarf table does not exist or there are no associated records,
1561 an empty list is returned
1567 return () unless dbdef->table('acct_snarf');
1568 eval "use FS::acct_snarf;";
1570 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1573 =item decrement_upbytes OCTETS
1575 Decrements the I<upbytes> field of this record by the given amount. If there
1576 is an error, returns the error, otherwise returns false.
1580 sub decrement_upbytes {
1581 shift->_op_usage('-', 'upbytes', @_);
1584 =item increment_upbytes OCTETS
1586 Increments the I<upbytes> field of this record by the given amount. If there
1587 is an error, returns the error, otherwise returns false.
1591 sub increment_upbytes {
1592 shift->_op_usage('+', 'upbytes', @_);
1595 =item decrement_downbytes OCTETS
1597 Decrements the I<downbytes> field of this record by the given amount. If there
1598 is an error, returns the error, otherwise returns false.
1602 sub decrement_downbytes {
1603 shift->_op_usage('-', 'downbytes', @_);
1606 =item increment_downbytes OCTETS
1608 Increments the I<downbytes> field of this record by the given amount. If there
1609 is an error, returns the error, otherwise returns false.
1613 sub increment_downbytes {
1614 shift->_op_usage('+', 'downbytes', @_);
1617 =item decrement_totalbytes OCTETS
1619 Decrements the I<totalbytes> field of this record by the given amount. If there
1620 is an error, returns the error, otherwise returns false.
1624 sub decrement_totalbytes {
1625 shift->_op_usage('-', 'totalbytes', @_);
1628 =item increment_totalbytes OCTETS
1630 Increments the I<totalbytes> field of this record by the given amount. If there
1631 is an error, returns the error, otherwise returns false.
1635 sub increment_totalbytes {
1636 shift->_op_usage('+', 'totalbytes', @_);
1639 =item decrement_seconds SECONDS
1641 Decrements the I<seconds> field of this record by the given amount. If there
1642 is an error, returns the error, otherwise returns false.
1646 sub decrement_seconds {
1647 shift->_op_usage('-', 'seconds', @_);
1650 =item increment_seconds SECONDS
1652 Increments the I<seconds> field of this record by the given amount. If there
1653 is an error, returns the error, otherwise returns false.
1657 sub increment_seconds {
1658 shift->_op_usage('+', 'seconds', @_);
1666 my %op2condition = (
1667 '-' => sub { my($self, $column, $amount) = @_;
1668 $self->$column - $amount <= 0;
1670 '+' => sub { my($self, $column, $amount) = @_;
1671 ($self->$column || 0) + $amount > 0;
1674 my %op2warncondition = (
1675 '-' => sub { my($self, $column, $amount) = @_;
1676 my $threshold = $column . '_threshold';
1677 $self->$column - $amount <= $self->$threshold + 0;
1679 '+' => sub { my($self, $column, $amount) = @_;
1680 ($self->$column || 0) + $amount > 0;
1685 my( $self, $op, $column, $amount ) = @_;
1687 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1688 ' ('. $self->email. "): $op $amount\n"
1691 return '' unless $amount;
1693 local $SIG{HUP} = 'IGNORE';
1694 local $SIG{INT} = 'IGNORE';
1695 local $SIG{QUIT} = 'IGNORE';
1696 local $SIG{TERM} = 'IGNORE';
1697 local $SIG{TSTP} = 'IGNORE';
1698 local $SIG{PIPE} = 'IGNORE';
1700 my $oldAutoCommit = $FS::UID::AutoCommit;
1701 local $FS::UID::AutoCommit = 0;
1704 my $sql = "UPDATE svc_acct SET $column = ".
1705 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1706 " $op ? WHERE svcnum = ?";
1710 my $sth = $dbh->prepare( $sql )
1711 or die "Error preparing $sql: ". $dbh->errstr;
1712 my $rv = $sth->execute($amount, $self->svcnum);
1713 die "Error executing $sql: ". $sth->errstr
1714 unless defined($rv);
1715 die "Can't update $column for svcnum". $self->svcnum
1718 #$self->snapshot; #not necessary, we retain the old values
1719 #create an object with the updated usage values
1720 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1722 my $error = $new->replace($self);
1724 $dbh->rollback if $oldAutoCommit;
1725 return "Error replacing: $error";
1728 #overlimit_action eq 'cancel' handling
1729 my $cust_pkg = $self->cust_svc->cust_pkg;
1731 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
1732 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
1736 my $error = $cust_pkg->cancel; #XXX should have a reason
1738 $dbh->rollback if $oldAutoCommit;
1739 return "Error cancelling: $error";
1742 #nothing else is relevant if we're cancelling, so commit & return success
1743 warn "$me update successful; committing\n"
1745 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1750 my $action = $op2action{$op};
1752 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1753 ( $action eq 'suspend' && !$self->overlimit
1754 || $action eq 'unsuspend' && $self->overlimit )
1756 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1757 if ($part_export->option('overlimit_groups')) {
1759 my $other = new FS::svc_acct $self->hashref;
1760 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1761 ($self, $part_export->option('overlimit_groups'));
1762 $other->usergroup( $groups );
1763 if ($action eq 'suspend'){
1764 $new = $other; $old = $self;
1766 $new = $self; $old = $other;
1768 my $error = $part_export->export_replace($new, $old);
1769 $error ||= $self->overlimit($action);
1771 $dbh->rollback if $oldAutoCommit;
1772 return "Error replacing radius groups in export, ${op}: $error";
1778 if ( $conf->exists("svc_acct-usage_$action")
1779 && &{$op2condition{$op}}($self, $column, $amount) ) {
1780 #my $error = $self->$action();
1781 my $error = $self->cust_svc->cust_pkg->$action();
1782 # $error ||= $self->overlimit($action);
1784 $dbh->rollback if $oldAutoCommit;
1785 return "Error ${action}ing: $error";
1789 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1790 my $wqueue = new FS::queue {
1791 'svcnum' => $self->svcnum,
1792 'job' => 'FS::svc_acct::reached_threshold',
1797 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1801 my $error = $wqueue->insert(
1802 'svcnum' => $self->svcnum,
1804 'column' => $column,
1808 $dbh->rollback if $oldAutoCommit;
1809 return "Error queuing threshold activity: $error";
1813 warn "$me update successful; committing\n"
1815 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1821 my( $self, $valueref, %options ) = @_;
1823 warn "$me set_usage called for svcnum ". $self->svcnum.
1824 ' ('. $self->email. "): ".
1825 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1828 local $SIG{HUP} = 'IGNORE';
1829 local $SIG{INT} = 'IGNORE';
1830 local $SIG{QUIT} = 'IGNORE';
1831 local $SIG{TERM} = 'IGNORE';
1832 local $SIG{TSTP} = 'IGNORE';
1833 local $SIG{PIPE} = 'IGNORE';
1835 local $FS::svc_Common::noexport_hack = 1;
1836 my $oldAutoCommit = $FS::UID::AutoCommit;
1837 local $FS::UID::AutoCommit = 0;
1842 if ( $options{null} ) {
1843 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1844 qw( seconds upbytes downbytes totalbytes )
1847 foreach my $field (keys %$valueref){
1848 $reset = 1 if $valueref->{$field};
1849 $self->setfield($field, $valueref->{$field});
1850 $self->setfield( $field.'_threshold',
1851 int($self->getfield($field)
1852 * ( $conf->exists('svc_acct-usage_threshold')
1853 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1858 $handyhash{$field} = $self->getfield($field);
1859 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1861 #my $error = $self->replace; #NO! we avoid the call to ->check for
1862 #die $error if $error; #services not explicity changed via the UI
1864 my $sql = "UPDATE svc_acct SET " .
1865 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1866 " WHERE svcnum = ". $self->svcnum;
1871 if (scalar(keys %handyhash)) {
1872 my $sth = $dbh->prepare( $sql )
1873 or die "Error preparing $sql: ". $dbh->errstr;
1874 my $rv = $sth->execute();
1875 die "Error executing $sql: ". $sth->errstr
1876 unless defined($rv);
1877 die "Can't update usage for svcnum ". $self->svcnum
1881 #$self->snapshot; #not necessary, we retain the old values
1882 #create an object with the updated usage values
1883 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1885 my $error = $new->replace($self);
1887 $dbh->rollback if $oldAutoCommit;
1888 return "Error replacing: $error";
1894 if ($self->overlimit) {
1895 $error = $self->overlimit('unsuspend');
1896 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1897 if ($part_export->option('overlimit_groups')) {
1898 my $old = new FS::svc_acct $self->hashref;
1899 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1900 ($self, $part_export->option('overlimit_groups'));
1901 $old->usergroup( $groups );
1902 $error ||= $part_export->export_replace($self, $old);
1907 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1908 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1911 $dbh->rollback if $oldAutoCommit;
1912 return "Error unsuspending: $error";
1916 warn "$me update successful; committing\n"
1918 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1924 =item recharge HASHREF
1926 Increments usage columns by the amount specified in HASHREF as
1927 column=>amount pairs.
1932 my ($self, $vhash) = @_;
1935 warn "[$me] recharge called on $self: ". Dumper($self).
1936 "\nwith vhash: ". Dumper($vhash);
1939 my $oldAutoCommit = $FS::UID::AutoCommit;
1940 local $FS::UID::AutoCommit = 0;
1944 foreach my $column (keys %$vhash){
1945 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1949 $dbh->rollback if $oldAutoCommit;
1951 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1956 =item is_rechargeable
1958 Returns true if this svc_account can be "recharged" and false otherwise.
1962 sub is_rechargable {
1964 $self->seconds ne ''
1965 || $self->upbytes ne ''
1966 || $self->downbytes ne ''
1967 || $self->totalbytes ne '';
1970 =item seconds_since TIMESTAMP
1972 Returns the number of seconds this account has been online since TIMESTAMP,
1973 according to the session monitor (see L<FS::Session>).
1975 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1976 L<Time::Local> and L<Date::Parse> for conversion functions.
1980 #note: POD here, implementation in FS::cust_svc
1983 $self->cust_svc->seconds_since(@_);
1986 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1988 Returns the numbers of seconds this account has been online between
1989 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1990 external SQL radacct table, specified via sqlradius export. Sessions which
1991 started in the specified range but are still open are counted from session
1992 start to the end of the range (unless they are over 1 day old, in which case
1993 they are presumed missing their stop record and not counted). Also, sessions
1994 which end in the range but started earlier are counted from the start of the
1995 range to session end. Finally, sessions which start before the range but end
1996 after are counted for the entire range.
1998 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1999 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2004 #note: POD here, implementation in FS::cust_svc
2005 sub seconds_since_sqlradacct {
2007 $self->cust_svc->seconds_since_sqlradacct(@_);
2010 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2012 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2013 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2014 TIMESTAMP_END (exclusive).
2016 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2017 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2022 #note: POD here, implementation in FS::cust_svc
2023 sub attribute_since_sqlradacct {
2025 $self->cust_svc->attribute_since_sqlradacct(@_);
2028 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2030 Returns an array of hash references of this customers login history for the
2031 given time range. (document this better)
2035 sub get_session_history {
2037 $self->cust_svc->get_session_history(@_);
2040 =item last_login_text
2042 Returns text describing the time of last login.
2046 sub last_login_text {
2048 $self->last_login ? ctime($self->last_login) : 'unknown';
2051 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2056 my($self, $start, $end, %opt ) = @_;
2058 my $did = $self->username; #yup
2060 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2062 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2064 #SELECT $for_update * FROM cdr
2065 # WHERE calldate >= $start #need a conversion
2066 # AND calldate < $end #ditto
2067 # AND ( charged_party = "$did"
2068 # OR charged_party = "$prefix$did" #if length($prefix);
2069 # OR ( ( charged_party IS NULL OR charged_party = '' )
2071 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2074 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2077 if ( length($prefix) ) {
2079 " AND ( charged_party = '$did'
2080 OR charged_party = '$prefix$did'
2081 OR ( ( charged_party IS NULL OR charged_party = '' )
2083 ( src = '$did' OR src = '$prefix$did' )
2089 " AND ( charged_party = '$did'
2090 OR ( ( charged_party IS NULL OR charged_party = '' )
2100 'select' => "$for_update *",
2103 #( freesidestatus IS NULL OR freesidestatus = '' )
2104 'freesidestatus' => '',
2106 'extra_sql' => $charged_or_src,
2114 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2120 if ( $self->usergroup ) {
2121 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2122 unless ref($self->usergroup) eq 'ARRAY';
2123 #when provisioning records, export callback runs in svc_Common.pm before
2124 #radius_usergroup records can be inserted...
2125 @{$self->usergroup};
2127 map { $_->groupname }
2128 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2132 =item clone_suspended
2134 Constructor used by FS::part_export::_export_suspend fallback. Document
2139 sub clone_suspended {
2141 my %hash = $self->hash;
2142 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2143 new FS::svc_acct \%hash;
2146 =item clone_kludge_unsuspend
2148 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2153 sub clone_kludge_unsuspend {
2155 my %hash = $self->hash;
2156 $hash{_password} = '';
2157 new FS::svc_acct \%hash;
2160 =item check_password
2162 Checks the supplied password against the (possibly encrypted) password in the
2163 database. Returns true for a successful authentication, false for no match.
2165 Currently supported encryptions are: classic DES crypt() and MD5
2169 sub check_password {
2170 my($self, $check_password) = @_;
2172 #remove old-style SUSPENDED kludge, they should be allowed to login to
2173 #self-service and pay up
2174 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2176 #eventually should check a "password-encoding" field
2177 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2179 } elsif ( length($password) < 13 ) { #plaintext
2180 $check_password eq $password;
2181 } elsif ( length($password) == 13 ) { #traditional DES crypt
2182 crypt($check_password, $password) eq $password;
2183 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2184 unix_md5_crypt($check_password, $password) eq $password;
2185 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2186 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
2187 $self->svcnum. "\n";
2190 warn "Can't check password: Unrecognized encryption for svcnum ".
2191 $self->svcnum. "\n";
2197 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2199 Returns an encrypted password, either by passing through an encrypted password
2200 in the database or by encrypting a plaintext password from the database.
2202 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2203 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2204 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2205 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2206 encryption type is only used if the password is not already encrypted in the
2211 sub crypt_password {
2213 #eventually should check a "password-encoding" field
2214 if ( length($self->_password) == 13
2215 || $self->_password =~ /^\$(1|2a?)\$/
2216 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2221 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2222 if ( $encryption eq 'crypt' ) {
2225 $saltset[int(rand(64))].$saltset[int(rand(64))]
2227 } elsif ( $encryption eq 'md5' ) {
2228 unix_md5_crypt( $self->_password );
2229 } elsif ( $encryption eq 'blowfish' ) {
2230 croak "unknown encryption method $encryption";
2232 croak "unknown encryption method $encryption";
2237 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2239 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2240 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
2241 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
2243 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2244 to work the same as the B</crypt_password> method.
2250 #eventually should check a "password-encoding" field
2251 if ( length($self->_password) == 13 ) { #crypt
2252 return '{CRYPT}'. $self->_password;
2253 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2255 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2256 warn "Blowfish encryption not supported in this context, svcnum ".
2257 $self->svcnum. "\n";
2258 return '{CRYPT}*'; #unsupported, should not auth
2259 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2260 return '{SSHA}'. $1;
2261 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2262 return '{NS-MTA-MD5}'. $1;
2264 return '{PLAIN}'. $self->_password;
2265 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2266 #if ( $encryption eq 'crypt' ) {
2267 # return '{CRYPT}'. 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";
2281 =item domain_slash_username
2283 Returns $domain/$username/
2287 sub domain_slash_username {
2289 $self->domain. '/'. $self->username. '/';
2292 =item virtual_maildir
2294 Returns $domain/maildirs/$username/
2298 sub virtual_maildir {
2300 $self->domain. '/maildirs/'. $self->username. '/';
2311 This is the FS::svc_acct job-queue-able version. It still uses
2312 FS::Misc::send_email under-the-hood.
2319 eval "use FS::Misc qw(send_email)";
2322 $opt{mimetype} ||= 'text/plain';
2323 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2325 my $error = send_email(
2326 'from' => $opt{from},
2328 'subject' => $opt{subject},
2329 'content-type' => $opt{mimetype},
2330 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2332 die $error if $error;
2335 =item check_and_rebuild_fuzzyfiles
2339 sub check_and_rebuild_fuzzyfiles {
2340 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2341 -e "$dir/svc_acct.username"
2342 or &rebuild_fuzzyfiles;
2345 =item rebuild_fuzzyfiles
2349 sub rebuild_fuzzyfiles {
2351 use Fcntl qw(:flock);
2353 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2357 open(USERNAMELOCK,">>$dir/svc_acct.username")
2358 or die "can't open $dir/svc_acct.username: $!";
2359 flock(USERNAMELOCK,LOCK_EX)
2360 or die "can't lock $dir/svc_acct.username: $!";
2362 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2364 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2365 or die "can't open $dir/svc_acct.username.tmp: $!";
2366 print USERNAMECACHE join("\n", @all_username), "\n";
2367 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2369 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2379 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2380 open(USERNAMECACHE,"<$dir/svc_acct.username")
2381 or die "can't open $dir/svc_acct.username: $!";
2382 my @array = map { chomp; $_; } <USERNAMECACHE>;
2383 close USERNAMECACHE;
2387 =item append_fuzzyfiles USERNAME
2391 sub append_fuzzyfiles {
2392 my $username = shift;
2394 &check_and_rebuild_fuzzyfiles;
2396 use Fcntl qw(:flock);
2398 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2400 open(USERNAME,">>$dir/svc_acct.username")
2401 or die "can't open $dir/svc_acct.username: $!";
2402 flock(USERNAME,LOCK_EX)
2403 or die "can't lock $dir/svc_acct.username: $!";
2405 print USERNAME "$username\n";
2407 flock(USERNAME,LOCK_UN)
2408 or die "can't unlock $dir/svc_acct.username: $!";
2416 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2420 sub radius_usergroup_selector {
2421 my $sel_groups = shift;
2422 my %sel_groups = map { $_=>1 } @$sel_groups;
2424 my $selectname = shift || 'radius_usergroup';
2427 my $sth = $dbh->prepare(
2428 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2429 ) or die $dbh->errstr;
2430 $sth->execute() or die $sth->errstr;
2431 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2435 function ${selectname}_doadd(object) {
2436 var myvalue = object.${selectname}_add.value;
2437 var optionName = new Option(myvalue,myvalue,false,true);
2438 var length = object.$selectname.length;
2439 object.$selectname.options[length] = optionName;
2440 object.${selectname}_add.value = "";
2443 <SELECT MULTIPLE NAME="$selectname">
2446 foreach my $group ( @all_groups ) {
2447 $html .= qq(<OPTION VALUE="$group");
2448 if ( $sel_groups{$group} ) {
2449 $html .= ' SELECTED';
2450 $sel_groups{$group} = 0;
2452 $html .= ">$group</OPTION>\n";
2454 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2455 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2457 $html .= '</SELECT>';
2459 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2460 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2465 =item reached_threshold
2467 Performs some activities when svc_acct thresholds (such as number of seconds
2468 remaining) are reached.
2472 sub reached_threshold {
2475 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2476 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2478 if ( $opt{'op'} eq '+' ){
2479 $svc_acct->setfield( $opt{'column'}.'_threshold',
2480 int($svc_acct->getfield($opt{'column'})
2481 * ( $conf->exists('svc_acct-usage_threshold')
2482 ? $conf->config('svc_acct-usage_threshold')/100
2487 my $error = $svc_acct->replace;
2488 die $error if $error;
2489 }elsif ( $opt{'op'} eq '-' ){
2491 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2492 return '' if ($threshold eq '' );
2494 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2495 my $error = $svc_acct->replace;
2496 die $error if $error; # email next time, i guess
2498 if ( $warning_template ) {
2499 eval "use FS::Misc qw(send_email)";
2502 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2503 my $cust_main = $cust_pkg->cust_main;
2505 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2506 $cust_main->invoicing_list,
2507 ($opt{'to'} ? $opt{'to'} : ())
2510 my $mimetype = $warning_mimetype;
2511 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2513 my $body = $warning_template->fill_in( HASH => {
2514 'custnum' => $cust_main->custnum,
2515 'username' => $svc_acct->username,
2516 'password' => $svc_acct->_password,
2517 'first' => $cust_main->first,
2518 'last' => $cust_main->getfield('last'),
2519 'pkg' => $cust_pkg->part_pkg->pkg,
2520 'column' => $opt{'column'},
2521 'amount' => $opt{'column'} =~/bytes/
2522 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2523 : $svc_acct->getfield($opt{'column'}),
2524 'threshold' => $opt{'column'} =~/bytes/
2525 ? FS::UI::bytecount::display_bytecount($threshold)
2530 my $error = send_email(
2531 'from' => $warning_from,
2533 'subject' => $warning_subject,
2534 'content-type' => $mimetype,
2535 'body' => [ map "$_\n", split("\n", $body) ],
2537 die $error if $error;
2540 die "unknown op: " . $opt{'op'};
2548 The $recref stuff in sub check should be cleaned up.
2550 The suspend, unsuspend and cancel methods update the database, but not the
2551 current object. This is probably a bug as it's unexpected and
2554 radius_usergroup_selector? putting web ui components in here? they should
2555 probably live somewhere else...
2557 insertion of RADIUS group stuff in insert could be done with child_objects now
2558 (would probably clean up export of them too)
2560 _op_usage and set_usage bypass the history... maybe they shouldn't
2564 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2565 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2566 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2567 L<freeside-queued>), L<FS::svc_acct_pop>,
2568 schema.html from the base documentation.
2572 =item domain_select_hash %OPTIONS
2574 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2575 may at present purchase.
2577 Currently available options are: I<pkgnum> I<svcpart>
2581 sub domain_select_hash {
2582 my ($self, %options) = @_;
2588 $part_svc = $self->part_svc;
2589 $cust_pkg = $self->cust_svc->cust_pkg
2593 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2594 if $options{'svcpart'};
2596 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2597 if $options{'pkgnum'};
2599 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2600 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2601 %domains = map { $_->svcnum => $_->domain }
2602 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2603 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2604 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2605 %domains = map { $_->svcnum => $_->domain }
2606 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2607 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2608 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2610 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2613 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2614 my $svc_domain = qsearchs('svc_domain',
2615 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2616 if ( $svc_domain ) {
2617 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2619 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2620 $part_svc->part_svc_column('domsvc')->columnvalue;