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;
535 my $error = $self->SUPER::insert(
536 'jobnums' => \@jobnums,
537 'child_objects' => $self->child_objects,
541 $dbh->rollback if $oldAutoCommit;
545 if ( $self->usergroup ) {
546 foreach my $groupname ( @{$self->usergroup} ) {
547 my $radius_usergroup = new FS::radius_usergroup ( {
548 svcnum => $self->svcnum,
549 groupname => $groupname,
551 my $error = $radius_usergroup->insert;
553 $dbh->rollback if $oldAutoCommit;
559 unless ( $skip_fuzzyfiles ) {
560 $error = $self->queue_fuzzyfiles_update;
562 $dbh->rollback if $oldAutoCommit;
563 return "updating fuzzy search cache: $error";
567 my $cust_pkg = $self->cust_svc->cust_pkg;
570 my $cust_main = $cust_pkg->cust_main;
572 if ( $conf->exists('emailinvoiceautoalways')
573 || $conf->exists('emailinvoiceauto')
574 && ! $cust_main->invoicing_list_emailonly
576 my @invoicing_list = $cust_main->invoicing_list;
577 push @invoicing_list, $self->email;
578 $cust_main->invoicing_list(\@invoicing_list);
583 if ( $welcome_template && $cust_pkg ) {
584 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
588 'custnum' => $self->custnum,
589 'username' => $self->username,
590 'password' => $self->_password,
591 'first' => $cust_main->first,
592 'last' => $cust_main->getfield('last'),
593 'pkg' => $cust_pkg->part_pkg->pkg,
595 my $wqueue = new FS::queue {
596 'svcnum' => $self->svcnum,
597 'job' => 'FS::svc_acct::send_email'
599 my $error = $wqueue->insert(
601 'from' => $welcome_from,
602 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
603 'mimetype' => $welcome_mimetype,
604 'body' => $welcome_template->fill_in( HASH => \%hash, ),
607 $dbh->rollback if $oldAutoCommit;
608 return "error queuing welcome email: $error";
611 if ( $options{'depend_jobnum'} ) {
612 warn "$me depend_jobnum found; adding to welcome email dependancies"
614 if ( ref($options{'depend_jobnum'}) ) {
615 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
616 "to welcome email dependancies"
618 push @jobnums, @{ $options{'depend_jobnum'} };
620 warn "$me adding job $options{'depend_jobnum'} ".
621 "to welcome email dependancies"
623 push @jobnums, $options{'depend_jobnum'};
627 foreach my $jobnum ( @jobnums ) {
628 my $error = $wqueue->depend_insert($jobnum);
630 $dbh->rollback if $oldAutoCommit;
631 return "error queuing welcome email job dependancy: $error";
641 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
645 # set usage fields and thresholds if unset but set in a package def
646 sub preinsert_hook_first {
649 return '' unless $self->pkgnum;
651 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
652 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
653 return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
655 my %values = $part_pkg->usage_valuehash;
656 my $multiplier = $conf->exists('svc_acct-usage_threshold')
657 ? 1 - $conf->config('svc_acct-usage_threshold')/100
658 : 0.20; #doesn't matter
660 foreach ( keys %values ) {
661 next if $self->getfield($_);
662 $self->setfield( $_, $values{$_} );
663 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
664 if $conf->exists('svc_acct-usage_threshold');
672 Deletes this account from the database. If there is an error, returns the
673 error, otherwise returns false.
675 The corresponding FS::cust_svc record will be deleted as well.
677 (TODOC: new exports!)
684 return "can't delete system account" if $self->_check_system;
686 return "Can't delete an account which is a (svc_forward) source!"
687 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
689 return "Can't delete an account which is a (svc_forward) destination!"
690 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
692 return "Can't delete an account with (svc_www) web service!"
693 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
695 # what about records in session ? (they should refer to history table)
697 local $SIG{HUP} = 'IGNORE';
698 local $SIG{INT} = 'IGNORE';
699 local $SIG{QUIT} = 'IGNORE';
700 local $SIG{TERM} = 'IGNORE';
701 local $SIG{TSTP} = 'IGNORE';
702 local $SIG{PIPE} = 'IGNORE';
704 my $oldAutoCommit = $FS::UID::AutoCommit;
705 local $FS::UID::AutoCommit = 0;
708 foreach my $cust_main_invoice (
709 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
711 unless ( defined($cust_main_invoice) ) {
712 warn "WARNING: something's wrong with qsearch";
715 my %hash = $cust_main_invoice->hash;
716 $hash{'dest'} = $self->email;
717 my $new = new FS::cust_main_invoice \%hash;
718 my $error = $new->replace($cust_main_invoice);
720 $dbh->rollback if $oldAutoCommit;
725 foreach my $svc_domain (
726 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
728 my %hash = new FS::svc_domain->hash;
729 $hash{'catchall'} = '';
730 my $new = new FS::svc_domain \%hash;
731 my $error = $new->replace($svc_domain);
733 $dbh->rollback if $oldAutoCommit;
738 my $error = $self->SUPER::delete;
740 $dbh->rollback if $oldAutoCommit;
744 foreach my $radius_usergroup (
745 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
747 my $error = $radius_usergroup->delete;
749 $dbh->rollback if $oldAutoCommit;
754 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
758 =item replace OLD_RECORD
760 Replaces OLD_RECORD with this one in the database. If there is an error,
761 returns the error, otherwise returns false.
763 The additional field I<usergroup> can optionally be defined; if so it should
764 contain an arrayref of group names. See L<FS::radius_usergroup>.
770 my ( $new, $old ) = ( shift, shift );
772 warn "$me replacing $old with $new\n" if $DEBUG;
774 # We absolutely have to have an old vs. new record to make this work.
775 if (!defined($old)) {
776 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
779 return "can't modify system account" if $old->_check_system;
782 #no warnings 'numeric'; #alas, a 5.006-ism
785 foreach my $xid (qw( uid gid )) {
787 return "Can't change $xid!"
788 if ! $conf->exists("svc_acct-edit_$xid")
789 && $old->$xid() != $new->$xid()
790 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
795 #change homdir when we change username
796 $new->setfield('dir', '') if $old->username ne $new->username;
798 local $SIG{HUP} = 'IGNORE';
799 local $SIG{INT} = 'IGNORE';
800 local $SIG{QUIT} = 'IGNORE';
801 local $SIG{TERM} = 'IGNORE';
802 local $SIG{TSTP} = 'IGNORE';
803 local $SIG{PIPE} = 'IGNORE';
805 my $oldAutoCommit = $FS::UID::AutoCommit;
806 local $FS::UID::AutoCommit = 0;
809 # redundant, but so $new->usergroup gets set
810 $error = $new->check;
811 return $error if $error;
813 $old->usergroup( [ $old->radius_groups ] );
815 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
816 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
818 if ( $new->usergroup ) {
819 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
820 my @newgroups = @{$new->usergroup};
821 foreach my $oldgroup ( @{$old->usergroup} ) {
822 if ( grep { $oldgroup eq $_ } @newgroups ) {
823 @newgroups = grep { $oldgroup ne $_ } @newgroups;
826 my $radius_usergroup = qsearchs('radius_usergroup', {
827 svcnum => $old->svcnum,
828 groupname => $oldgroup,
830 my $error = $radius_usergroup->delete;
832 $dbh->rollback if $oldAutoCommit;
833 return "error deleting radius_usergroup $oldgroup: $error";
837 foreach my $newgroup ( @newgroups ) {
838 my $radius_usergroup = new FS::radius_usergroup ( {
839 svcnum => $new->svcnum,
840 groupname => $newgroup,
842 my $error = $radius_usergroup->insert;
844 $dbh->rollback if $oldAutoCommit;
845 return "error adding radius_usergroup $newgroup: $error";
851 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
852 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
853 $error = $new->_check_duplicate;
855 $dbh->rollback if $oldAutoCommit;
860 $error = $new->SUPER::replace($old);
862 $dbh->rollback if $oldAutoCommit;
863 return $error if $error;
866 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
867 $error = $new->queue_fuzzyfiles_update;
869 $dbh->rollback if $oldAutoCommit;
870 return "updating fuzzy search cache: $error";
874 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
878 =item queue_fuzzyfiles_update
880 Used by insert & replace to update the fuzzy search cache
884 sub queue_fuzzyfiles_update {
887 local $SIG{HUP} = 'IGNORE';
888 local $SIG{INT} = 'IGNORE';
889 local $SIG{QUIT} = 'IGNORE';
890 local $SIG{TERM} = 'IGNORE';
891 local $SIG{TSTP} = 'IGNORE';
892 local $SIG{PIPE} = 'IGNORE';
894 my $oldAutoCommit = $FS::UID::AutoCommit;
895 local $FS::UID::AutoCommit = 0;
898 my $queue = new FS::queue {
899 'svcnum' => $self->svcnum,
900 'job' => 'FS::svc_acct::append_fuzzyfiles'
902 my $error = $queue->insert($self->username);
904 $dbh->rollback if $oldAutoCommit;
905 return "queueing job (transaction rolled back): $error";
908 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
916 Suspends this account by calling export-specific suspend hooks. If there is
917 an error, returns the error, otherwise returns false.
919 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
925 return "can't suspend system account" if $self->_check_system;
926 $self->SUPER::suspend;
931 Unsuspends this account by by calling export-specific suspend hooks. If there
932 is an error, returns the error, otherwise returns false.
934 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
940 my %hash = $self->hash;
941 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
942 $hash{_password} = $1;
943 my $new = new FS::svc_acct ( \%hash );
944 my $error = $new->replace($self);
945 return $error if $error;
948 $self->SUPER::unsuspend;
953 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
955 If the B<auto_unset_catchall> configuration option is set, this method will
956 automatically remove any references to the canceled service in the catchall
957 field of svc_domain. This allows packages that contain both a svc_domain and
958 its catchall svc_acct to be canceled in one step.
963 # Only one thing to do at this level
965 foreach my $svc_domain (
966 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
967 if($conf->exists('auto_unset_catchall')) {
968 my %hash = $svc_domain->hash;
969 $hash{catchall} = '';
970 my $new = new FS::svc_domain ( \%hash );
971 my $error = $new->replace($svc_domain);
972 return $error if $error;
974 return "cannot unprovision svc_acct #".$self->svcnum.
975 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
979 $self->SUPER::cancel;
985 Checks all fields to make sure this is a valid service. If there is an error,
986 returns the error, otherwise returns false. Called by the insert and replace
989 Sets any fixed values; see L<FS::part_svc>.
996 my($recref) = $self->hashref;
998 my $x = $self->setfixed( $self->_fieldhandlers );
999 return $x unless ref($x);
1002 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1004 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1007 my $error = $self->ut_numbern('svcnum')
1008 #|| $self->ut_number('domsvc')
1009 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1010 || $self->ut_textn('sec_phrase')
1011 || $self->ut_snumbern('seconds')
1012 || $self->ut_snumbern('upbytes')
1013 || $self->ut_snumbern('downbytes')
1014 || $self->ut_snumbern('totalbytes')
1016 return $error if $error;
1018 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1019 if ( $username_uppercase ) {
1020 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1021 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1022 $recref->{username} = $1;
1024 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1025 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1026 $recref->{username} = $1;
1029 if ( $username_letterfirst ) {
1030 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1031 } elsif ( $username_letter ) {
1032 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1034 if ( $username_noperiod ) {
1035 $recref->{username} =~ /\./ and return gettext('illegal_username');
1037 if ( $username_nounderscore ) {
1038 $recref->{username} =~ /_/ and return gettext('illegal_username');
1040 if ( $username_nodash ) {
1041 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1043 unless ( $username_ampersand ) {
1044 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1046 if ( $password_noampersand ) {
1047 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1049 if ( $password_noexclamation ) {
1050 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1052 unless ( $username_percent ) {
1053 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1055 unless ( $username_colon ) {
1056 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1059 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1060 $recref->{popnum} = $1;
1061 return "Unknown popnum" unless
1062 ! $recref->{popnum} ||
1063 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1065 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1067 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1068 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1070 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1071 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1072 #not all systems use gid=uid
1073 #you can set a fixed gid in part_svc
1075 return "Only root can have uid 0"
1076 if $recref->{uid} == 0
1077 && $recref->{username} !~ /^(root|toor|smtp)$/;
1079 unless ( $recref->{username} eq 'sync' ) {
1080 if ( grep $_ eq $recref->{shell}, @shells ) {
1081 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1083 return "Illegal shell \`". $self->shell. "\'; ".
1084 $conf->dir. "/shells contains: @shells";
1087 $recref->{shell} = '/bin/sync';
1091 $recref->{gid} ne '' ?
1092 return "Can't have gid without uid" : ( $recref->{gid}='' );
1093 #$recref->{dir} ne '' ?
1094 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1095 $recref->{shell} ne '' ?
1096 return "Can't have shell without uid" : ( $recref->{shell}='' );
1099 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1101 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1102 or return "Illegal directory: ". $recref->{dir};
1103 $recref->{dir} = $1;
1104 return "Illegal directory"
1105 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1106 return "Illegal directory"
1107 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1108 unless ( $recref->{dir} ) {
1109 $recref->{dir} = $dir_prefix . '/';
1110 if ( $dirhash > 0 ) {
1111 for my $h ( 1 .. $dirhash ) {
1112 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1114 } elsif ( $dirhash < 0 ) {
1115 for my $h ( reverse $dirhash .. -1 ) {
1116 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1119 $recref->{dir} .= $recref->{username};
1125 # $error = $self->ut_textn('finger');
1126 # return $error if $error;
1127 if ( $self->getfield('finger') eq '' ) {
1128 my $cust_pkg = $self->svcnum
1129 ? $self->cust_svc->cust_pkg
1130 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1132 my $cust_main = $cust_pkg->cust_main;
1133 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1136 $self->getfield('finger') =~
1137 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1138 or return "Illegal finger: ". $self->getfield('finger');
1139 $self->setfield('finger', $1);
1141 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1142 $recref->{quota} = $1;
1144 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1145 if ( $recref->{slipip} eq '' ) {
1146 $recref->{slipip} = '';
1147 } elsif ( $recref->{slipip} eq '0e0' ) {
1148 $recref->{slipip} = '0e0';
1150 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1151 or return "Illegal slipip: ". $self->slipip;
1152 $recref->{slipip} = $1;
1157 #arbitrary RADIUS stuff; allow ut_textn for now
1158 foreach ( grep /^radius_/, fields('svc_acct') ) {
1159 $self->ut_textn($_);
1162 #generate a password if it is blank
1163 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1164 unless ( $recref->{_password} );
1166 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1167 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1168 $recref->{_password} = $1.$3;
1169 #uncomment this to encrypt password immediately upon entry, or run
1170 #bin/crypt_pw in cron to give new users a window during which their
1171 #password is available to techs, for faxing, etc. (also be aware of
1173 #$recref->{password} = $1.
1174 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1176 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1177 $recref->{_password} = $1.$3;
1178 } elsif ( $recref->{_password} eq '*' ) {
1179 $recref->{_password} = '*';
1180 } elsif ( $recref->{_password} eq '!' ) {
1181 $recref->{_password} = '!';
1182 } elsif ( $recref->{_password} eq '!!' ) {
1183 $recref->{_password} = '!!';
1185 #return "Illegal password";
1186 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1187 FS::Msgcat::_gettext('illegal_password_characters').
1188 ": ". $recref->{_password};
1191 $self->SUPER::check;
1196 Internal function to check the username against the list of system usernames
1197 from the I<system_usernames> configuration value. Returns true if the username
1198 is listed on the system username list.
1204 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1205 $conf->config('system_usernames')
1209 =item _check_duplicate
1211 Internal function to check for duplicates usernames, username@domain pairs and
1214 If the I<global_unique-username> configuration value is set to B<username> or
1215 B<username@domain>, enforces global username or username@domain uniqueness.
1217 In all cases, check for duplicate uids and usernames or username@domain pairs
1218 per export and with identical I<svcpart> values.
1222 sub _check_duplicate {
1225 my $global_unique = $conf->config('global_unique-username') || 'none';
1226 return '' if $global_unique eq 'disabled';
1228 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1229 if ( driver_name =~ /^Pg/i ) {
1230 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1232 } elsif ( driver_name =~ /^mysql/i ) {
1233 dbh->do("SELECT * FROM duplicate_lock
1234 WHERE lockname = 'svc_acct'
1236 ) or die dbh->errstr;
1238 die "unknown database ". driver_name.
1239 "; don't know how to lock for duplicate search";
1241 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1243 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1244 unless ( $part_svc ) {
1245 return 'unknown svcpart '. $self->svcpart;
1248 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1249 qsearch( 'svc_acct', { 'username' => $self->username } );
1250 return gettext('username_in_use')
1251 if $global_unique eq 'username' && @dup_user;
1253 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1254 qsearch( 'svc_acct', { 'username' => $self->username,
1255 'domsvc' => $self->domsvc } );
1256 return gettext('username_in_use')
1257 if $global_unique eq 'username@domain' && @dup_userdomain;
1260 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1261 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1262 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1263 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1268 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1269 my $exports = FS::part_export::export_info('svc_acct');
1270 my %conflict_user_svcpart;
1271 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1273 foreach my $part_export ( $part_svc->part_export ) {
1275 #this will catch to the same exact export
1276 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1278 #this will catch to exports w/same exporthost+type ???
1279 #my @other_part_export = qsearch('part_export', {
1280 # 'machine' => $part_export->machine,
1281 # 'exporttype' => $part_export->exporttype,
1283 #foreach my $other_part_export ( @other_part_export ) {
1284 # push @svcparts, map { $_->svcpart }
1285 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1288 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1289 #silly kludge to avoid uninitialized value errors
1290 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1291 ? $exports->{$part_export->exporttype}{'nodomain'}
1293 if ( $nodomain =~ /^Y/i ) {
1294 $conflict_user_svcpart{$_} = $part_export->exportnum
1297 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1302 foreach my $dup_user ( @dup_user ) {
1303 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1304 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1305 return "duplicate username ". $self->username.
1306 ": conflicts with svcnum ". $dup_user->svcnum.
1307 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1311 foreach my $dup_userdomain ( @dup_userdomain ) {
1312 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1313 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1314 return "duplicate username\@domain ". $self->email.
1315 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1316 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1320 foreach my $dup_uid ( @dup_uid ) {
1321 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1322 if ( exists($conflict_user_svcpart{$dup_svcpart})
1323 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1324 return "duplicate uid ". $self->uid.
1325 ": conflicts with svcnum ". $dup_uid->svcnum.
1327 ( $conflict_user_svcpart{$dup_svcpart}
1328 || $conflict_userdomain_svcpart{$dup_svcpart} );
1340 Depriciated, use radius_reply instead.
1345 carp "FS::svc_acct::radius depriciated, use radius_reply";
1346 $_[0]->radius_reply;
1351 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1352 reply attributes of this record.
1354 Note that this is now the preferred method for reading RADIUS attributes -
1355 accessing the columns directly is discouraged, as the column names are
1356 expected to change in the future.
1363 return %{ $self->{'radius_reply'} }
1364 if exists $self->{'radius_reply'};
1369 my($column, $attrib) = ($1, $2);
1370 #$attrib =~ s/_/\-/g;
1371 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1372 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1374 if ( $self->slipip && $self->slipip ne '0e0' ) {
1375 $reply{$radius_ip} = $self->slipip;
1378 if ( $self->seconds !~ /^$/ ) {
1379 $reply{'Session-Timeout'} = $self->seconds;
1382 if ( $conf->exists('radius-chillispot-max') ) {
1383 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1385 #hmm. just because sqlradius.pm says so?
1392 foreach my $what (qw( input output total )) {
1393 my $is = $whatis{$what}.'bytes';
1394 if ( $self->$is() =~ /\d/ ) {
1395 my $big = new Math::BigInt $self->$is();
1396 $big = new Math::BigInt '0' if $big->is_neg();
1397 my $att = "Chillispot-Max-\u$what";
1398 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1399 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1410 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1411 check attributes of this record.
1413 Note that this is now the preferred method for reading RADIUS attributes -
1414 accessing the columns directly is discouraged, as the column names are
1415 expected to change in the future.
1422 return %{ $self->{'radius_check'} }
1423 if exists $self->{'radius_check'};
1428 my($column, $attrib) = ($1, $2);
1429 #$attrib =~ s/_/\-/g;
1430 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1431 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1433 my $password = $self->_password;
1434 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1436 my $cust_svc = $self->cust_svc;
1438 my $cust_pkg = $cust_svc->cust_pkg;
1439 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1440 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1443 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1444 "; can't set Expiration\n"
1454 This method instructs the object to "snapshot" or freeze RADIUS check and
1455 reply attributes to the current values.
1459 #bah, my english is too broken this morning
1460 #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
1461 #the FS::cust_pkg's replace method to trigger the correct export updates when
1462 #package dates change)
1467 $self->{$_} = { $self->$_() }
1468 foreach qw( radius_reply radius_check );
1472 =item forget_snapshot
1474 This methos instructs the object to forget any previously snapshotted
1475 RADIUS check and reply attributes.
1479 sub forget_snapshot {
1483 foreach qw( radius_reply radius_check );
1487 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1489 Returns the domain associated with this account.
1491 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1498 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1499 my $svc_domain = $self->svc_domain(@_)
1500 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1501 $svc_domain->domain;
1506 Returns the FS::svc_domain record for this account's domain (see
1511 # FS::h_svc_acct has a history-aware svc_domain override
1516 ? $self->{'_domsvc'}
1517 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1522 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1526 #inherited from svc_Common
1528 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1530 Returns an email address associated with the account.
1532 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1539 $self->username. '@'. $self->domain(@_);
1544 Returns an array of FS::acct_snarf records associated with the account.
1545 If the acct_snarf table does not exist or there are no associated records,
1546 an empty list is returned
1552 return () unless dbdef->table('acct_snarf');
1553 eval "use FS::acct_snarf;";
1555 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1558 =item decrement_upbytes OCTETS
1560 Decrements the I<upbytes> field of this record by the given amount. If there
1561 is an error, returns the error, otherwise returns false.
1565 sub decrement_upbytes {
1566 shift->_op_usage('-', 'upbytes', @_);
1569 =item increment_upbytes OCTETS
1571 Increments the I<upbytes> field of this record by the given amount. If there
1572 is an error, returns the error, otherwise returns false.
1576 sub increment_upbytes {
1577 shift->_op_usage('+', 'upbytes', @_);
1580 =item decrement_downbytes OCTETS
1582 Decrements the I<downbytes> field of this record by the given amount. If there
1583 is an error, returns the error, otherwise returns false.
1587 sub decrement_downbytes {
1588 shift->_op_usage('-', 'downbytes', @_);
1591 =item increment_downbytes OCTETS
1593 Increments the I<downbytes> field of this record by the given amount. If there
1594 is an error, returns the error, otherwise returns false.
1598 sub increment_downbytes {
1599 shift->_op_usage('+', 'downbytes', @_);
1602 =item decrement_totalbytes OCTETS
1604 Decrements the I<totalbytes> field of this record by the given amount. If there
1605 is an error, returns the error, otherwise returns false.
1609 sub decrement_totalbytes {
1610 shift->_op_usage('-', 'totalbytes', @_);
1613 =item increment_totalbytes OCTETS
1615 Increments the I<totalbytes> field of this record by the given amount. If there
1616 is an error, returns the error, otherwise returns false.
1620 sub increment_totalbytes {
1621 shift->_op_usage('+', 'totalbytes', @_);
1624 =item decrement_seconds SECONDS
1626 Decrements the I<seconds> field of this record by the given amount. If there
1627 is an error, returns the error, otherwise returns false.
1631 sub decrement_seconds {
1632 shift->_op_usage('-', 'seconds', @_);
1635 =item increment_seconds SECONDS
1637 Increments the I<seconds> field of this record by the given amount. If there
1638 is an error, returns the error, otherwise returns false.
1642 sub increment_seconds {
1643 shift->_op_usage('+', 'seconds', @_);
1651 my %op2condition = (
1652 '-' => sub { my($self, $column, $amount) = @_;
1653 $self->$column - $amount <= 0;
1655 '+' => sub { my($self, $column, $amount) = @_;
1656 ($self->$column || 0) + $amount > 0;
1659 my %op2warncondition = (
1660 '-' => sub { my($self, $column, $amount) = @_;
1661 my $threshold = $column . '_threshold';
1662 $self->$column - $amount <= $self->$threshold + 0;
1664 '+' => sub { my($self, $column, $amount) = @_;
1665 ($self->$column || 0) + $amount > 0;
1670 my( $self, $op, $column, $amount ) = @_;
1672 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1673 ' ('. $self->email. "): $op $amount\n"
1676 return '' unless $amount;
1678 local $SIG{HUP} = 'IGNORE';
1679 local $SIG{INT} = 'IGNORE';
1680 local $SIG{QUIT} = 'IGNORE';
1681 local $SIG{TERM} = 'IGNORE';
1682 local $SIG{TSTP} = 'IGNORE';
1683 local $SIG{PIPE} = 'IGNORE';
1685 my $oldAutoCommit = $FS::UID::AutoCommit;
1686 local $FS::UID::AutoCommit = 0;
1689 my $sql = "UPDATE svc_acct SET $column = ".
1690 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1691 " $op ? WHERE svcnum = ?";
1695 my $sth = $dbh->prepare( $sql )
1696 or die "Error preparing $sql: ". $dbh->errstr;
1697 my $rv = $sth->execute($amount, $self->svcnum);
1698 die "Error executing $sql: ". $sth->errstr
1699 unless defined($rv);
1700 die "Can't update $column for svcnum". $self->svcnum
1703 #$self->snapshot; #not necessary, we retain the old values
1704 #create an object with the updated usage values
1705 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1707 my $error = $new->replace($self);
1709 $dbh->rollback if $oldAutoCommit;
1710 return "Error replacing: $error";
1713 #overlimit_action eq 'cancel' handling
1714 my $cust_pkg = $self->cust_svc->cust_pkg;
1716 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
1717 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
1721 my $error = $cust_pkg->cancel; #XXX should have a reason
1723 $dbh->rollback if $oldAutoCommit;
1724 return "Error cancelling: $error";
1727 #nothing else is relevant if we're cancelling, so commit & return success
1728 warn "$me update successful; committing\n"
1730 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1735 my $action = $op2action{$op};
1737 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1738 ( $action eq 'suspend' && !$self->overlimit
1739 || $action eq 'unsuspend' && $self->overlimit )
1742 my $error = $self->_op_overlimit($action);
1744 $dbh->rollback if $oldAutoCommit;
1750 if ( $conf->exists("svc_acct-usage_$action")
1751 && &{$op2condition{$op}}($self, $column, $amount) ) {
1752 #my $error = $self->$action();
1753 my $error = $self->cust_svc->cust_pkg->$action();
1754 # $error ||= $self->overlimit($action);
1756 $dbh->rollback if $oldAutoCommit;
1757 return "Error ${action}ing: $error";
1761 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1762 my $wqueue = new FS::queue {
1763 'svcnum' => $self->svcnum,
1764 'job' => 'FS::svc_acct::reached_threshold',
1769 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1773 my $error = $wqueue->insert(
1774 'svcnum' => $self->svcnum,
1776 'column' => $column,
1780 $dbh->rollback if $oldAutoCommit;
1781 return "Error queuing threshold activity: $error";
1785 warn "$me update successful; committing\n"
1787 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1793 my( $self, $action ) = @_;
1795 local $SIG{HUP} = 'IGNORE';
1796 local $SIG{INT} = 'IGNORE';
1797 local $SIG{QUIT} = 'IGNORE';
1798 local $SIG{TERM} = 'IGNORE';
1799 local $SIG{TSTP} = 'IGNORE';
1800 local $SIG{PIPE} = 'IGNORE';
1802 my $oldAutoCommit = $FS::UID::AutoCommit;
1803 local $FS::UID::AutoCommit = 0;
1806 my $cust_pkg = $self->cust_svc->cust_pkg;
1808 my $conf_overlimit = $conf->config('overlimit_groups');
1810 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1812 my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
1813 next unless $groups;
1815 my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
1817 my $other = new FS::svc_acct $self->hashref;
1818 $other->usergroup( $gref );
1821 if ($action eq 'suspend') {
1824 } else { # $action eq 'unsuspend'
1829 my $error = $part_export->export_replace($new, $old)
1830 || $self->overlimit($action);
1833 $dbh->rollback if $oldAutoCommit;
1834 return "Error replacing radius groups: $error";
1839 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1845 my( $self, $valueref, %options ) = @_;
1847 warn "$me set_usage called for svcnum ". $self->svcnum.
1848 ' ('. $self->email. "): ".
1849 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1852 local $SIG{HUP} = 'IGNORE';
1853 local $SIG{INT} = 'IGNORE';
1854 local $SIG{QUIT} = 'IGNORE';
1855 local $SIG{TERM} = 'IGNORE';
1856 local $SIG{TSTP} = 'IGNORE';
1857 local $SIG{PIPE} = 'IGNORE';
1859 local $FS::svc_Common::noexport_hack = 1;
1860 my $oldAutoCommit = $FS::UID::AutoCommit;
1861 local $FS::UID::AutoCommit = 0;
1866 if ( $options{null} ) {
1867 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1868 qw( seconds upbytes downbytes totalbytes )
1871 foreach my $field (keys %$valueref){
1872 $reset = 1 if $valueref->{$field};
1873 $self->setfield($field, $valueref->{$field});
1874 $self->setfield( $field.'_threshold',
1875 int($self->getfield($field)
1876 * ( $conf->exists('svc_acct-usage_threshold')
1877 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1882 $handyhash{$field} = $self->getfield($field);
1883 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1885 #my $error = $self->replace; #NO! we avoid the call to ->check for
1886 #die $error if $error; #services not explicity changed via the UI
1888 my $sql = "UPDATE svc_acct SET " .
1889 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1890 " WHERE svcnum = ". $self->svcnum;
1895 if (scalar(keys %handyhash)) {
1896 my $sth = $dbh->prepare( $sql )
1897 or die "Error preparing $sql: ". $dbh->errstr;
1898 my $rv = $sth->execute();
1899 die "Error executing $sql: ". $sth->errstr
1900 unless defined($rv);
1901 die "Can't update usage for svcnum ". $self->svcnum
1905 #$self->snapshot; #not necessary, we retain the old values
1906 #create an object with the updated usage values
1907 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1909 my $error = $new->replace($self);
1911 $dbh->rollback if $oldAutoCommit;
1912 return "Error replacing: $error";
1919 $error = $self->_op_overlimit('unsuspend')
1920 if $self->overlimit;;
1922 $error ||= $self->cust_svc->cust_pkg->unsuspend
1923 if $conf->exists("svc_acct-usage_unsuspend");
1926 $dbh->rollback if $oldAutoCommit;
1927 return "Error unsuspending: $error";
1932 warn "$me update successful; committing\n"
1934 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1940 =item recharge HASHREF
1942 Increments usage columns by the amount specified in HASHREF as
1943 column=>amount pairs.
1948 my ($self, $vhash) = @_;
1951 warn "[$me] recharge called on $self: ". Dumper($self).
1952 "\nwith vhash: ". Dumper($vhash);
1955 my $oldAutoCommit = $FS::UID::AutoCommit;
1956 local $FS::UID::AutoCommit = 0;
1960 foreach my $column (keys %$vhash){
1961 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1965 $dbh->rollback if $oldAutoCommit;
1967 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1972 =item is_rechargeable
1974 Returns true if this svc_account can be "recharged" and false otherwise.
1978 sub is_rechargable {
1980 $self->seconds ne ''
1981 || $self->upbytes ne ''
1982 || $self->downbytes ne ''
1983 || $self->totalbytes ne '';
1986 =item seconds_since TIMESTAMP
1988 Returns the number of seconds this account has been online since TIMESTAMP,
1989 according to the session monitor (see L<FS::Session>).
1991 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1992 L<Time::Local> and L<Date::Parse> for conversion functions.
1996 #note: POD here, implementation in FS::cust_svc
1999 $self->cust_svc->seconds_since(@_);
2002 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
2004 Returns the numbers of seconds this account has been online between
2005 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
2006 external SQL radacct table, specified via sqlradius export. Sessions which
2007 started in the specified range but are still open are counted from session
2008 start to the end of the range (unless they are over 1 day old, in which case
2009 they are presumed missing their stop record and not counted). Also, sessions
2010 which end in the range but started earlier are counted from the start of the
2011 range to session end. Finally, sessions which start before the range but end
2012 after are counted for the entire range.
2014 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2015 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2020 #note: POD here, implementation in FS::cust_svc
2021 sub seconds_since_sqlradacct {
2023 $self->cust_svc->seconds_since_sqlradacct(@_);
2026 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2028 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2029 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2030 TIMESTAMP_END (exclusive).
2032 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2033 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2038 #note: POD here, implementation in FS::cust_svc
2039 sub attribute_since_sqlradacct {
2041 $self->cust_svc->attribute_since_sqlradacct(@_);
2044 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2046 Returns an array of hash references of this customers login history for the
2047 given time range. (document this better)
2051 sub get_session_history {
2053 $self->cust_svc->get_session_history(@_);
2056 =item last_login_text
2058 Returns text describing the time of last login.
2062 sub last_login_text {
2064 $self->last_login ? ctime($self->last_login) : 'unknown';
2067 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2072 my($self, $start, $end, %opt ) = @_;
2074 my $did = $self->username; #yup
2076 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2078 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2080 #SELECT $for_update * FROM cdr
2081 # WHERE calldate >= $start #need a conversion
2082 # AND calldate < $end #ditto
2083 # AND ( charged_party = "$did"
2084 # OR charged_party = "$prefix$did" #if length($prefix);
2085 # OR ( ( charged_party IS NULL OR charged_party = '' )
2087 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2090 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2093 if ( length($prefix) ) {
2095 " AND ( charged_party = '$did'
2096 OR charged_party = '$prefix$did'
2097 OR ( ( charged_party IS NULL OR charged_party = '' )
2099 ( src = '$did' OR src = '$prefix$did' )
2105 " AND ( charged_party = '$did'
2106 OR ( ( charged_party IS NULL OR charged_party = '' )
2116 'select' => "$for_update *",
2119 #( freesidestatus IS NULL OR freesidestatus = '' )
2120 'freesidestatus' => '',
2122 'extra_sql' => $charged_or_src,
2130 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2136 if ( $self->usergroup ) {
2137 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2138 unless ref($self->usergroup) eq 'ARRAY';
2139 #when provisioning records, export callback runs in svc_Common.pm before
2140 #radius_usergroup records can be inserted...
2141 @{$self->usergroup};
2143 map { $_->groupname }
2144 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2148 =item clone_suspended
2150 Constructor used by FS::part_export::_export_suspend fallback. Document
2155 sub clone_suspended {
2157 my %hash = $self->hash;
2158 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2159 new FS::svc_acct \%hash;
2162 =item clone_kludge_unsuspend
2164 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2169 sub clone_kludge_unsuspend {
2171 my %hash = $self->hash;
2172 $hash{_password} = '';
2173 new FS::svc_acct \%hash;
2176 =item check_password
2178 Checks the supplied password against the (possibly encrypted) password in the
2179 database. Returns true for a successful authentication, false for no match.
2181 Currently supported encryptions are: classic DES crypt() and MD5
2185 sub check_password {
2186 my($self, $check_password) = @_;
2188 #remove old-style SUSPENDED kludge, they should be allowed to login to
2189 #self-service and pay up
2190 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2192 #eventually should check a "password-encoding" field
2193 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2195 } elsif ( length($password) < 13 ) { #plaintext
2196 $check_password eq $password;
2197 } elsif ( length($password) == 13 ) { #traditional DES crypt
2198 crypt($check_password, $password) eq $password;
2199 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2200 unix_md5_crypt($check_password, $password) eq $password;
2201 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2202 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
2203 $self->svcnum. "\n";
2206 warn "Can't check password: Unrecognized encryption for svcnum ".
2207 $self->svcnum. "\n";
2213 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2215 Returns an encrypted password, either by passing through an encrypted password
2216 in the database or by encrypting a plaintext password from the database.
2218 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2219 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2220 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2221 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2222 encryption type is only used if the password is not already encrypted in the
2227 sub crypt_password {
2229 #eventually should check a "password-encoding" field
2230 if ( length($self->_password) == 13
2231 || $self->_password =~ /^\$(1|2a?)\$/
2232 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2237 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2238 if ( $encryption eq 'crypt' ) {
2241 $saltset[int(rand(64))].$saltset[int(rand(64))]
2243 } elsif ( $encryption eq 'md5' ) {
2244 unix_md5_crypt( $self->_password );
2245 } elsif ( $encryption eq 'blowfish' ) {
2246 croak "unknown encryption method $encryption";
2248 croak "unknown encryption method $encryption";
2253 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2255 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2256 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
2257 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
2259 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2260 to work the same as the B</crypt_password> method.
2266 #eventually should check a "password-encoding" field
2267 if ( length($self->_password) == 13 ) { #crypt
2268 return '{CRYPT}'. $self->_password;
2269 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2271 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2272 warn "Blowfish encryption not supported in this context, svcnum ".
2273 $self->svcnum. "\n";
2274 return '{CRYPT}*'; #unsupported, should not auth
2275 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2276 return '{SSHA}'. $1;
2277 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2278 return '{NS-MTA-MD5}'. $1;
2280 return '{PLAIN}'. $self->_password;
2281 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2282 #if ( $encryption eq 'crypt' ) {
2283 # return '{CRYPT}'. crypt(
2285 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2287 #} elsif ( $encryption eq 'md5' ) {
2288 # unix_md5_crypt( $self->_password );
2289 #} elsif ( $encryption eq 'blowfish' ) {
2290 # croak "unknown encryption method $encryption";
2292 # croak "unknown encryption method $encryption";
2297 =item domain_slash_username
2299 Returns $domain/$username/
2303 sub domain_slash_username {
2305 $self->domain. '/'. $self->username. '/';
2308 =item virtual_maildir
2310 Returns $domain/maildirs/$username/
2314 sub virtual_maildir {
2316 $self->domain. '/maildirs/'. $self->username. '/';
2327 This is the FS::svc_acct job-queue-able version. It still uses
2328 FS::Misc::send_email under-the-hood.
2335 eval "use FS::Misc qw(send_email)";
2338 $opt{mimetype} ||= 'text/plain';
2339 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2341 my $error = send_email(
2342 'from' => $opt{from},
2344 'subject' => $opt{subject},
2345 'content-type' => $opt{mimetype},
2346 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2348 die $error if $error;
2351 =item check_and_rebuild_fuzzyfiles
2355 sub check_and_rebuild_fuzzyfiles {
2356 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2357 -e "$dir/svc_acct.username"
2358 or &rebuild_fuzzyfiles;
2361 =item rebuild_fuzzyfiles
2365 sub rebuild_fuzzyfiles {
2367 use Fcntl qw(:flock);
2369 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2373 open(USERNAMELOCK,">>$dir/svc_acct.username")
2374 or die "can't open $dir/svc_acct.username: $!";
2375 flock(USERNAMELOCK,LOCK_EX)
2376 or die "can't lock $dir/svc_acct.username: $!";
2378 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2380 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2381 or die "can't open $dir/svc_acct.username.tmp: $!";
2382 print USERNAMECACHE join("\n", @all_username), "\n";
2383 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2385 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2395 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2396 open(USERNAMECACHE,"<$dir/svc_acct.username")
2397 or die "can't open $dir/svc_acct.username: $!";
2398 my @array = map { chomp; $_; } <USERNAMECACHE>;
2399 close USERNAMECACHE;
2403 =item append_fuzzyfiles USERNAME
2407 sub append_fuzzyfiles {
2408 my $username = shift;
2410 &check_and_rebuild_fuzzyfiles;
2412 use Fcntl qw(:flock);
2414 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2416 open(USERNAME,">>$dir/svc_acct.username")
2417 or die "can't open $dir/svc_acct.username: $!";
2418 flock(USERNAME,LOCK_EX)
2419 or die "can't lock $dir/svc_acct.username: $!";
2421 print USERNAME "$username\n";
2423 flock(USERNAME,LOCK_UN)
2424 or die "can't unlock $dir/svc_acct.username: $!";
2432 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2436 sub radius_usergroup_selector {
2437 my $sel_groups = shift;
2438 my %sel_groups = map { $_=>1 } @$sel_groups;
2440 my $selectname = shift || 'radius_usergroup';
2443 my $sth = $dbh->prepare(
2444 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2445 ) or die $dbh->errstr;
2446 $sth->execute() or die $sth->errstr;
2447 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2451 function ${selectname}_doadd(object) {
2452 var myvalue = object.${selectname}_add.value;
2453 var optionName = new Option(myvalue,myvalue,false,true);
2454 var length = object.$selectname.length;
2455 object.$selectname.options[length] = optionName;
2456 object.${selectname}_add.value = "";
2459 <SELECT MULTIPLE NAME="$selectname">
2462 foreach my $group ( @all_groups ) {
2463 $html .= qq(<OPTION VALUE="$group");
2464 if ( $sel_groups{$group} ) {
2465 $html .= ' SELECTED';
2466 $sel_groups{$group} = 0;
2468 $html .= ">$group</OPTION>\n";
2470 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2471 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2473 $html .= '</SELECT>';
2475 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2476 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2481 =item reached_threshold
2483 Performs some activities when svc_acct thresholds (such as number of seconds
2484 remaining) are reached.
2488 sub reached_threshold {
2491 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2492 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2494 if ( $opt{'op'} eq '+' ){
2495 $svc_acct->setfield( $opt{'column'}.'_threshold',
2496 int($svc_acct->getfield($opt{'column'})
2497 * ( $conf->exists('svc_acct-usage_threshold')
2498 ? $conf->config('svc_acct-usage_threshold')/100
2503 my $error = $svc_acct->replace;
2504 die $error if $error;
2505 }elsif ( $opt{'op'} eq '-' ){
2507 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2508 return '' if ($threshold eq '' );
2510 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2511 my $error = $svc_acct->replace;
2512 die $error if $error; # email next time, i guess
2514 if ( $warning_template ) {
2515 eval "use FS::Misc qw(send_email)";
2518 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2519 my $cust_main = $cust_pkg->cust_main;
2521 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2522 $cust_main->invoicing_list,
2523 ($opt{'to'} ? $opt{'to'} : ())
2526 my $mimetype = $warning_mimetype;
2527 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2529 my $body = $warning_template->fill_in( HASH => {
2530 'custnum' => $cust_main->custnum,
2531 'username' => $svc_acct->username,
2532 'password' => $svc_acct->_password,
2533 'first' => $cust_main->first,
2534 'last' => $cust_main->getfield('last'),
2535 'pkg' => $cust_pkg->part_pkg->pkg,
2536 'column' => $opt{'column'},
2537 'amount' => $opt{'column'} =~/bytes/
2538 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2539 : $svc_acct->getfield($opt{'column'}),
2540 'threshold' => $opt{'column'} =~/bytes/
2541 ? FS::UI::bytecount::display_bytecount($threshold)
2546 my $error = send_email(
2547 'from' => $warning_from,
2549 'subject' => $warning_subject,
2550 'content-type' => $mimetype,
2551 'body' => [ map "$_\n", split("\n", $body) ],
2553 die $error if $error;
2556 die "unknown op: " . $opt{'op'};
2564 The $recref stuff in sub check should be cleaned up.
2566 The suspend, unsuspend and cancel methods update the database, but not the
2567 current object. This is probably a bug as it's unexpected and
2570 radius_usergroup_selector? putting web ui components in here? they should
2571 probably live somewhere else...
2573 insertion of RADIUS group stuff in insert could be done with child_objects now
2574 (would probably clean up export of them too)
2576 _op_usage and set_usage bypass the history... maybe they shouldn't
2580 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2581 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2582 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2583 L<freeside-queued>), L<FS::svc_acct_pop>,
2584 schema.html from the base documentation.
2588 =item domain_select_hash %OPTIONS
2590 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2591 may at present purchase.
2593 Currently available options are: I<pkgnum> I<svcpart>
2597 sub domain_select_hash {
2598 my ($self, %options) = @_;
2604 $part_svc = $self->part_svc;
2605 $cust_pkg = $self->cust_svc->cust_pkg
2609 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2610 if $options{'svcpart'};
2612 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2613 if $options{'pkgnum'};
2615 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2616 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2617 %domains = map { $_->svcnum => $_->domain }
2618 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2619 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2620 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2621 %domains = map { $_->svcnum => $_->domain }
2622 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2623 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2624 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2626 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2629 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2630 my $svc_domain = qsearchs('svc_domain',
2631 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2632 if ( $svc_domain ) {
2633 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2635 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2636 $part_svc->part_svc_column('domsvc')->columnvalue;