4 use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
5 $dir_prefix @shells $usernamemin
6 $usernamemax $passwordmin $passwordmax
7 $username_ampersand $username_letter $username_letterfirst
8 $username_noperiod $username_nounderscore $username_nodash
9 $username_uppercase $username_percent
10 $password_noampersand $password_noexclamation
11 $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
22 use Crypt::PasswdMD5 1.2;
24 use FS::UID qw( datasrc );
26 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
27 use FS::Msgcat qw(gettext);
32 use FS::cust_main_invoice;
36 use FS::radius_usergroup;
43 @ISA = qw( FS::svc_Common );
46 $me = '[FS::svc_acct]';
48 #ask FS::UID to run this stuff for us later
49 $FS::UID::callback{'FS::svc_acct'} = sub {
51 $dir_prefix = $conf->config('home');
52 @shells = $conf->config('shells');
53 $usernamemin = $conf->config('usernamemin') || 2;
54 $usernamemax = $conf->config('usernamemax');
55 $passwordmin = $conf->config('passwordmin') || 6;
56 $passwordmax = $conf->config('passwordmax') || 8;
57 $username_letter = $conf->exists('username-letter');
58 $username_letterfirst = $conf->exists('username-letterfirst');
59 $username_noperiod = $conf->exists('username-noperiod');
60 $username_nounderscore = $conf->exists('username-nounderscore');
61 $username_nodash = $conf->exists('username-nodash');
62 $username_uppercase = $conf->exists('username-uppercase');
63 $username_ampersand = $conf->exists('username-ampersand');
64 $username_percent = $conf->exists('username-percent');
65 $password_noampersand = $conf->exists('password-noexclamation');
66 $password_noexclamation = $conf->exists('password-noexclamation');
67 $dirhash = $conf->config('dirhash') || 0;
68 if ( $conf->exists('welcome_email') ) {
69 $welcome_template = new Text::Template (
71 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
72 ) or warn "can't create welcome email template: $Text::Template::ERROR";
73 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
74 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
75 $welcome_subject_template = new Text::Template (
77 SOURCE => $welcome_subject,
78 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
79 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
81 $welcome_template = '';
83 $welcome_subject = '';
84 $welcome_mimetype = '';
86 if ( $conf->exists('warning_email') ) {
87 $warning_template = new Text::Template (
89 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
90 ) or warn "can't create warning email template: $Text::Template::ERROR";
91 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
92 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
93 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
94 $warning_cc = $conf->config('warning_email-cc');
96 $warning_template = '';
98 $warning_subject = '';
99 $warning_mimetype = '';
102 $smtpmachine = $conf->config('smtpmachine');
103 $radius_password = $conf->config('radius-password') || 'Password';
104 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
107 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
108 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
112 my ( $hashref, $cache ) = @_;
113 if ( $hashref->{'svc_acct_svcnum'} ) {
114 $self->{'_domsvc'} = FS::svc_domain->new( {
115 'svcnum' => $hashref->{'domsvc'},
116 'domain' => $hashref->{'svc_acct_domain'},
117 'catchall' => $hashref->{'svc_acct_catchall'},
124 FS::svc_acct - Object methods for svc_acct records
130 $record = new FS::svc_acct \%hash;
131 $record = new FS::svc_acct { 'column' => 'value' };
133 $error = $record->insert;
135 $error = $new_record->replace($old_record);
137 $error = $record->delete;
139 $error = $record->check;
141 $error = $record->suspend;
143 $error = $record->unsuspend;
145 $error = $record->cancel;
147 %hash = $record->radius;
149 %hash = $record->radius_reply;
151 %hash = $record->radius_check;
153 $domain = $record->domain;
155 $svc_domain = $record->svc_domain;
157 $email = $record->email;
159 $seconds_since = $record->seconds_since($timestamp);
163 An FS::svc_acct object represents an account. FS::svc_acct inherits from
164 FS::svc_Common. The following fields are currently supported:
168 =item svcnum - primary key (assigned automatcially for new accounts)
172 =item _password - generated if blank
174 =item sec_phrase - security phrase
176 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
184 =item dir - set automatically if blank (and uid is not)
188 =item quota - (unimplementd)
190 =item slipip - IP address
200 =item domsvc - svcnum from svc_domain
202 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
204 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
214 Creates a new account. To add the account to the database, see L<"insert">.
221 'longname_plural' => 'Access accounts and mailboxes',
222 'sorts' => [ 'username', 'uid', ],
223 'display_weight' => 10,
224 'cancel_weight' => 50,
226 'dir' => 'Home directory',
229 def_label => 'UID (set to fixed and blank for no UIDs)',
232 'slipip' => 'IP address',
233 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
235 label => 'Access number',
237 select_table => 'svc_acct_pop',
238 select_key => 'popnum',
239 select_label => 'city',
245 disable_default => 1,
252 disable_inventory => 1,
255 '_password' => 'Password',
258 def_label => 'GID (when blank, defaults to UID)',
262 #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)',
264 def_label=> 'Shell (set to blank for no shell tracking)',
266 select_list => [ $conf->config('shells') ],
267 disable_inventory => 1,
270 'finger' => 'Real name (GECOS)',
273 #def_label => 'svcnum from svc_domain',
275 select_table => 'svc_domain',
276 select_key => 'svcnum',
277 select_label => 'domain',
278 disable_inventory => 1,
282 label => 'RADIUS groups',
283 type => 'radius_usergroup_selector',
284 disable_inventory => 1,
287 'seconds' => { label => 'Seconds',
289 disable_inventory => 1,
296 sub table { 'svc_acct'; }
300 #false laziness with edit/svc_acct.cgi
302 my( $self, $groups ) = @_;
303 if ( ref($groups) eq 'ARRAY' ) {
305 } elsif ( length($groups) ) {
306 [ split(/\s*,\s*/, $groups) ];
314 =item search_sql STRING
316 Class method which returns an SQL fragment to search for the given string.
321 my( $class, $string ) = @_;
322 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
323 my( $username, $domain ) = ( $1, $2 );
324 my $q_username = dbh->quote($username);
325 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
327 "svc_acct.username = $q_username AND ( ".
328 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
333 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
335 $class->search_sql_field('slipip', $string ).
337 $class->search_sql_field('username', $string ).
340 $class->search_sql_field('username', $string);
344 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
346 Returns the "username@domain" string for this account.
348 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
360 =item insert [ , OPTION => VALUE ... ]
362 Adds this account to the database. If there is an error, returns the error,
363 otherwise returns false.
365 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
366 defined. An FS::cust_svc record will be created and inserted.
368 The additional field I<usergroup> can optionally be defined; if so it should
369 contain an arrayref of group names. See L<FS::radius_usergroup>.
371 The additional field I<child_objects> can optionally be defined; if so it
372 should contain an arrayref of FS::tablename objects. They will have their
373 svcnum fields set and will be inserted after this record, but before any
374 exports are run. Each element of the array can also optionally be a
375 two-element array reference containing the child object and the name of an
376 alternate field to be filled in with the newly-inserted svcnum, for example
377 C<[ $svc_forward, 'srcsvc' ]>
379 Currently available options are: I<depend_jobnum>
381 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
382 jobnums), all provisioning jobs will have a dependancy on the supplied
383 jobnum(s) (they will not run until the specific job(s) complete(s)).
385 (TODOC: L<FS::queue> and L<freeside-queued>)
387 (TODOC: new exports!)
396 warn "[$me] insert called on $self: ". Dumper($self).
397 "\nwith options: ". Dumper(%options);
400 local $SIG{HUP} = 'IGNORE';
401 local $SIG{INT} = 'IGNORE';
402 local $SIG{QUIT} = 'IGNORE';
403 local $SIG{TERM} = 'IGNORE';
404 local $SIG{TSTP} = 'IGNORE';
405 local $SIG{PIPE} = 'IGNORE';
407 my $oldAutoCommit = $FS::UID::AutoCommit;
408 local $FS::UID::AutoCommit = 0;
411 my $error = $self->check;
412 return $error if $error;
414 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
415 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
416 unless ( $cust_svc ) {
417 $dbh->rollback if $oldAutoCommit;
418 return "no cust_svc record found for svcnum ". $self->svcnum;
420 $self->pkgnum($cust_svc->pkgnum);
421 $self->svcpart($cust_svc->svcpart);
424 $error = $self->_check_duplicate;
426 $dbh->rollback if $oldAutoCommit;
431 $error = $self->SUPER::insert(
432 'jobnums' => \@jobnums,
433 'child_objects' => $self->child_objects,
437 $dbh->rollback if $oldAutoCommit;
441 if ( $self->usergroup ) {
442 foreach my $groupname ( @{$self->usergroup} ) {
443 my $radius_usergroup = new FS::radius_usergroup ( {
444 svcnum => $self->svcnum,
445 groupname => $groupname,
447 my $error = $radius_usergroup->insert;
449 $dbh->rollback if $oldAutoCommit;
455 unless ( $skip_fuzzyfiles ) {
456 $error = $self->queue_fuzzyfiles_update;
458 $dbh->rollback if $oldAutoCommit;
459 return "updating fuzzy search cache: $error";
463 my $cust_pkg = $self->cust_svc->cust_pkg;
466 my $cust_main = $cust_pkg->cust_main;
468 if ( $conf->exists('emailinvoiceautoalways')
469 || $conf->exists('emailinvoiceauto')
470 && ! $cust_main->invoicing_list_emailonly
472 my @invoicing_list = $cust_main->invoicing_list;
473 push @invoicing_list, $self->email;
474 $cust_main->invoicing_list(\@invoicing_list);
479 if ( $welcome_template && $cust_pkg ) {
480 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
484 'custnum' => $self->custnum,
485 'username' => $self->username,
486 'password' => $self->_password,
487 'first' => $cust_main->first,
488 'last' => $cust_main->getfield('last'),
489 'pkg' => $cust_pkg->part_pkg->pkg,
491 my $wqueue = new FS::queue {
492 'svcnum' => $self->svcnum,
493 'job' => 'FS::svc_acct::send_email'
495 my $error = $wqueue->insert(
497 'from' => $welcome_from,
498 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
499 'mimetype' => $welcome_mimetype,
500 'body' => $welcome_template->fill_in( HASH => \%hash, ),
503 $dbh->rollback if $oldAutoCommit;
504 return "error queuing welcome email: $error";
507 if ( $options{'depend_jobnum'} ) {
508 warn "$me depend_jobnum found; adding to welcome email dependancies"
510 if ( ref($options{'depend_jobnum'}) ) {
511 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
512 "to welcome email dependancies"
514 push @jobnums, @{ $options{'depend_jobnum'} };
516 warn "$me adding job $options{'depend_jobnum'} ".
517 "to welcome email dependancies"
519 push @jobnums, $options{'depend_jobnum'};
523 foreach my $jobnum ( @jobnums ) {
524 my $error = $wqueue->depend_insert($jobnum);
526 $dbh->rollback if $oldAutoCommit;
527 return "error queuing welcome email job dependancy: $error";
537 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
543 Deletes this account from the database. If there is an error, returns the
544 error, otherwise returns false.
546 The corresponding FS::cust_svc record will be deleted as well.
548 (TODOC: new exports!)
555 return "can't delete system account" if $self->_check_system;
557 return "Can't delete an account which is a (svc_forward) source!"
558 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
560 return "Can't delete an account which is a (svc_forward) destination!"
561 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
563 return "Can't delete an account with (svc_www) web service!"
564 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
566 # what about records in session ? (they should refer to history table)
568 local $SIG{HUP} = 'IGNORE';
569 local $SIG{INT} = 'IGNORE';
570 local $SIG{QUIT} = 'IGNORE';
571 local $SIG{TERM} = 'IGNORE';
572 local $SIG{TSTP} = 'IGNORE';
573 local $SIG{PIPE} = 'IGNORE';
575 my $oldAutoCommit = $FS::UID::AutoCommit;
576 local $FS::UID::AutoCommit = 0;
579 foreach my $cust_main_invoice (
580 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
582 unless ( defined($cust_main_invoice) ) {
583 warn "WARNING: something's wrong with qsearch";
586 my %hash = $cust_main_invoice->hash;
587 $hash{'dest'} = $self->email;
588 my $new = new FS::cust_main_invoice \%hash;
589 my $error = $new->replace($cust_main_invoice);
591 $dbh->rollback if $oldAutoCommit;
596 foreach my $svc_domain (
597 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
599 my %hash = new FS::svc_domain->hash;
600 $hash{'catchall'} = '';
601 my $new = new FS::svc_domain \%hash;
602 my $error = $new->replace($svc_domain);
604 $dbh->rollback if $oldAutoCommit;
609 foreach my $radius_usergroup (
610 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
612 my $error = $radius_usergroup->delete;
614 $dbh->rollback if $oldAutoCommit;
619 my $error = $self->SUPER::delete;
621 $dbh->rollback if $oldAutoCommit;
625 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
629 =item replace OLD_RECORD
631 Replaces OLD_RECORD with this one in the database. If there is an error,
632 returns the error, otherwise returns false.
634 The additional field I<usergroup> can optionally be defined; if so it should
635 contain an arrayref of group names. See L<FS::radius_usergroup>.
641 my ( $new, $old ) = ( shift, shift );
643 warn "$me replacing $old with $new\n" if $DEBUG;
645 # We absolutely have to have an old vs. new record to make this work.
646 if (!defined($old)) {
647 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
650 return "can't modify system account" if $old->_check_system;
653 #no warnings 'numeric'; #alas, a 5.006-ism
656 foreach my $xid (qw( uid gid )) {
658 return "Can't change $xid!"
659 if ! $conf->exists("svc_acct-edit_$xid")
660 && $old->$xid() != $new->$xid()
661 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
666 #change homdir when we change username
667 $new->setfield('dir', '') if $old->username ne $new->username;
669 local $SIG{HUP} = 'IGNORE';
670 local $SIG{INT} = 'IGNORE';
671 local $SIG{QUIT} = 'IGNORE';
672 local $SIG{TERM} = 'IGNORE';
673 local $SIG{TSTP} = 'IGNORE';
674 local $SIG{PIPE} = 'IGNORE';
676 my $oldAutoCommit = $FS::UID::AutoCommit;
677 local $FS::UID::AutoCommit = 0;
680 # redundant, but so $new->usergroup gets set
681 $error = $new->check;
682 return $error if $error;
684 $old->usergroup( [ $old->radius_groups ] );
686 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
687 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
689 if ( $new->usergroup ) {
690 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
691 my @newgroups = @{$new->usergroup};
692 foreach my $oldgroup ( @{$old->usergroup} ) {
693 if ( grep { $oldgroup eq $_ } @newgroups ) {
694 @newgroups = grep { $oldgroup ne $_ } @newgroups;
697 my $radius_usergroup = qsearchs('radius_usergroup', {
698 svcnum => $old->svcnum,
699 groupname => $oldgroup,
701 my $error = $radius_usergroup->delete;
703 $dbh->rollback if $oldAutoCommit;
704 return "error deleting radius_usergroup $oldgroup: $error";
708 foreach my $newgroup ( @newgroups ) {
709 my $radius_usergroup = new FS::radius_usergroup ( {
710 svcnum => $new->svcnum,
711 groupname => $newgroup,
713 my $error = $radius_usergroup->insert;
715 $dbh->rollback if $oldAutoCommit;
716 return "error adding radius_usergroup $newgroup: $error";
722 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
723 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
724 $error = $new->_check_duplicate;
726 $dbh->rollback if $oldAutoCommit;
731 $error = $new->SUPER::replace($old);
733 $dbh->rollback if $oldAutoCommit;
734 return $error if $error;
737 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
738 $error = $new->queue_fuzzyfiles_update;
740 $dbh->rollback if $oldAutoCommit;
741 return "updating fuzzy search cache: $error";
745 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
749 =item queue_fuzzyfiles_update
751 Used by insert & replace to update the fuzzy search cache
755 sub queue_fuzzyfiles_update {
758 local $SIG{HUP} = 'IGNORE';
759 local $SIG{INT} = 'IGNORE';
760 local $SIG{QUIT} = 'IGNORE';
761 local $SIG{TERM} = 'IGNORE';
762 local $SIG{TSTP} = 'IGNORE';
763 local $SIG{PIPE} = 'IGNORE';
765 my $oldAutoCommit = $FS::UID::AutoCommit;
766 local $FS::UID::AutoCommit = 0;
769 my $queue = new FS::queue {
770 'svcnum' => $self->svcnum,
771 'job' => 'FS::svc_acct::append_fuzzyfiles'
773 my $error = $queue->insert($self->username);
775 $dbh->rollback if $oldAutoCommit;
776 return "queueing job (transaction rolled back): $error";
779 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
787 Suspends this account by calling export-specific suspend hooks. If there is
788 an error, returns the error, otherwise returns false.
790 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
796 return "can't suspend system account" if $self->_check_system;
797 $self->SUPER::suspend;
802 Unsuspends this account by by calling export-specific suspend hooks. If there
803 is an error, returns the error, otherwise returns false.
805 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
811 my %hash = $self->hash;
812 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
813 $hash{_password} = $1;
814 my $new = new FS::svc_acct ( \%hash );
815 my $error = $new->replace($self);
816 return $error if $error;
819 $self->SUPER::unsuspend;
824 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
826 If the B<auto_unset_catchall> configuration option is set, this method will
827 automatically remove any references to the canceled service in the catchall
828 field of svc_domain. This allows packages that contain both a svc_domain and
829 its catchall svc_acct to be canceled in one step.
834 # Only one thing to do at this level
836 foreach my $svc_domain (
837 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
838 if($conf->exists('auto_unset_catchall')) {
839 my %hash = $svc_domain->hash;
840 $hash{catchall} = '';
841 my $new = new FS::svc_domain ( \%hash );
842 my $error = $new->replace($svc_domain);
843 return $error if $error;
845 return "cannot unprovision svc_acct #".$self->svcnum.
846 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
850 $self->SUPER::cancel;
856 Checks all fields to make sure this is a valid service. If there is an error,
857 returns the error, otherwise returns false. Called by the insert and replace
860 Sets any fixed values; see L<FS::part_svc>.
867 my($recref) = $self->hashref;
869 my $x = $self->setfixed( $self->_fieldhandlers );
870 return $x unless ref($x);
873 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
875 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
878 my $error = $self->ut_numbern('svcnum')
879 #|| $self->ut_number('domsvc')
880 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
881 || $self->ut_textn('sec_phrase')
882 || $self->ut_snumbern('seconds')
883 || $self->ut_snumbern('upbytes')
884 || $self->ut_snumbern('downbytes')
885 || $self->ut_snumbern('totalbytes')
887 return $error if $error;
889 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
890 if ( $username_uppercase ) {
891 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
892 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
893 $recref->{username} = $1;
895 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/
896 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
897 $recref->{username} = $1;
900 if ( $username_letterfirst ) {
901 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
902 } elsif ( $username_letter ) {
903 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
905 if ( $username_noperiod ) {
906 $recref->{username} =~ /\./ and return gettext('illegal_username');
908 if ( $username_nounderscore ) {
909 $recref->{username} =~ /_/ and return gettext('illegal_username');
911 if ( $username_nodash ) {
912 $recref->{username} =~ /\-/ and return gettext('illegal_username');
914 unless ( $username_ampersand ) {
915 $recref->{username} =~ /\&/ and return gettext('illegal_username');
917 if ( $password_noampersand ) {
918 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
920 if ( $password_noexclamation ) {
921 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
923 unless ( $username_percent ) {
924 $recref->{username} =~ /\%/ and return gettext('illegal_username');
927 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
928 $recref->{popnum} = $1;
929 return "Unknown popnum" unless
930 ! $recref->{popnum} ||
931 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
933 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
935 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
936 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
938 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
939 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
940 #not all systems use gid=uid
941 #you can set a fixed gid in part_svc
943 return "Only root can have uid 0"
944 if $recref->{uid} == 0
945 && $recref->{username} !~ /^(root|toor|smtp)$/;
947 unless ( $recref->{username} eq 'sync' ) {
948 if ( grep $_ eq $recref->{shell}, @shells ) {
949 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
951 return "Illegal shell \`". $self->shell. "\'; ".
952 $conf->dir. "/shells contains: @shells";
955 $recref->{shell} = '/bin/sync';
959 $recref->{gid} ne '' ?
960 return "Can't have gid without uid" : ( $recref->{gid}='' );
961 #$recref->{dir} ne '' ?
962 # return "Can't have directory without uid" : ( $recref->{dir}='' );
963 $recref->{shell} ne '' ?
964 return "Can't have shell without uid" : ( $recref->{shell}='' );
967 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
969 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
970 or return "Illegal directory: ". $recref->{dir};
972 return "Illegal directory"
973 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
974 return "Illegal directory"
975 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
976 unless ( $recref->{dir} ) {
977 $recref->{dir} = $dir_prefix . '/';
978 if ( $dirhash > 0 ) {
979 for my $h ( 1 .. $dirhash ) {
980 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
982 } elsif ( $dirhash < 0 ) {
983 for my $h ( reverse $dirhash .. -1 ) {
984 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
987 $recref->{dir} .= $recref->{username};
993 # $error = $self->ut_textn('finger');
994 # return $error if $error;
995 if ( $self->getfield('finger') eq '' ) {
996 my $cust_pkg = $self->svcnum
997 ? $self->cust_svc->cust_pkg
998 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1000 my $cust_main = $cust_pkg->cust_main;
1001 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1004 $self->getfield('finger') =~
1005 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1006 or return "Illegal finger: ". $self->getfield('finger');
1007 $self->setfield('finger', $1);
1009 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1010 $recref->{quota} = $1;
1012 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1013 if ( $recref->{slipip} eq '' ) {
1014 $recref->{slipip} = '';
1015 } elsif ( $recref->{slipip} eq '0e0' ) {
1016 $recref->{slipip} = '0e0';
1018 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1019 or return "Illegal slipip: ". $self->slipip;
1020 $recref->{slipip} = $1;
1025 #arbitrary RADIUS stuff; allow ut_textn for now
1026 foreach ( grep /^radius_/, fields('svc_acct') ) {
1027 $self->ut_textn($_);
1030 #generate a password if it is blank
1031 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1032 unless ( $recref->{_password} );
1034 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1035 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1036 $recref->{_password} = $1.$3;
1037 #uncomment this to encrypt password immediately upon entry, or run
1038 #bin/crypt_pw in cron to give new users a window during which their
1039 #password is available to techs, for faxing, etc. (also be aware of
1041 #$recref->{password} = $1.
1042 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1044 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1045 $recref->{_password} = $1.$3;
1046 } elsif ( $recref->{_password} eq '*' ) {
1047 $recref->{_password} = '*';
1048 } elsif ( $recref->{_password} eq '!' ) {
1049 $recref->{_password} = '!';
1050 } elsif ( $recref->{_password} eq '!!' ) {
1051 $recref->{_password} = '!!';
1053 #return "Illegal password";
1054 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1055 FS::Msgcat::_gettext('illegal_password_characters').
1056 ": ". $recref->{_password};
1059 $self->SUPER::check;
1064 Internal function to check the username against the list of system usernames
1065 from the I<system_usernames> configuration value. Returns true if the username
1066 is listed on the system username list.
1072 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1073 $conf->config('system_usernames')
1077 =item _check_duplicate
1079 Internal function to check for duplicates usernames, username@domain pairs and
1082 If the I<global_unique-username> configuration value is set to B<username> or
1083 B<username@domain>, enforces global username or username@domain uniqueness.
1085 In all cases, check for duplicate uids and usernames or username@domain pairs
1086 per export and with identical I<svcpart> values.
1090 sub _check_duplicate {
1093 my $global_unique = $conf->config('global_unique-username') || 'none';
1094 return '' if $global_unique eq 'disabled';
1096 #this is Pg-specific. what to do for mysql etc?
1097 # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
1098 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1099 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1101 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1103 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1104 unless ( $part_svc ) {
1105 return 'unknown svcpart '. $self->svcpart;
1108 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1109 qsearch( 'svc_acct', { 'username' => $self->username } );
1110 return gettext('username_in_use')
1111 if $global_unique eq 'username' && @dup_user;
1113 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1114 qsearch( 'svc_acct', { 'username' => $self->username,
1115 'domsvc' => $self->domsvc } );
1116 return gettext('username_in_use')
1117 if $global_unique eq 'username@domain' && @dup_userdomain;
1120 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1121 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1122 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1123 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1128 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1129 my $exports = FS::part_export::export_info('svc_acct');
1130 my %conflict_user_svcpart;
1131 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1133 foreach my $part_export ( $part_svc->part_export ) {
1135 #this will catch to the same exact export
1136 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1138 #this will catch to exports w/same exporthost+type ???
1139 #my @other_part_export = qsearch('part_export', {
1140 # 'machine' => $part_export->machine,
1141 # 'exporttype' => $part_export->exporttype,
1143 #foreach my $other_part_export ( @other_part_export ) {
1144 # push @svcparts, map { $_->svcpart }
1145 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1148 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1149 #silly kludge to avoid uninitialized value errors
1150 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1151 ? $exports->{$part_export->exporttype}{'nodomain'}
1153 if ( $nodomain =~ /^Y/i ) {
1154 $conflict_user_svcpart{$_} = $part_export->exportnum
1157 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1162 foreach my $dup_user ( @dup_user ) {
1163 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1164 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1165 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
1166 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1170 foreach my $dup_userdomain ( @dup_userdomain ) {
1171 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1172 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1173 return "duplicate username\@domain: conflicts with svcnum ".
1174 $dup_userdomain->svcnum. " via exportnum ".
1175 $conflict_userdomain_svcpart{$dup_svcpart};
1179 foreach my $dup_uid ( @dup_uid ) {
1180 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1181 if ( exists($conflict_user_svcpart{$dup_svcpart})
1182 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1183 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
1184 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
1185 || $conflict_userdomain_svcpart{$dup_svcpart};
1197 Depriciated, use radius_reply instead.
1202 carp "FS::svc_acct::radius depriciated, use radius_reply";
1203 $_[0]->radius_reply;
1208 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1209 reply attributes of this record.
1211 Note that this is now the preferred method for reading RADIUS attributes -
1212 accessing the columns directly is discouraged, as the column names are
1213 expected to change in the future.
1220 return %{ $self->{'radius_reply'} }
1221 if exists $self->{'radius_reply'};
1226 my($column, $attrib) = ($1, $2);
1227 #$attrib =~ s/_/\-/g;
1228 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1229 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1231 if ( $self->slipip && $self->slipip ne '0e0' ) {
1232 $reply{$radius_ip} = $self->slipip;
1235 if ( $self->seconds !~ /^$/ ) {
1236 $reply{'Session-Timeout'} = $self->seconds;
1244 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1245 check attributes of this record.
1247 Note that this is now the preferred method for reading RADIUS attributes -
1248 accessing the columns directly is discouraged, as the column names are
1249 expected to change in the future.
1256 return %{ $self->{'radius_check'} }
1257 if exists $self->{'radius_check'};
1262 my($column, $attrib) = ($1, $2);
1263 #$attrib =~ s/_/\-/g;
1264 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1265 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1267 my $password = $self->_password;
1268 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1270 my $cust_svc = $self->cust_svc;
1271 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1273 my $cust_pkg = $cust_svc->cust_pkg;
1274 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1275 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1284 This method instructs the object to "snapshot" or freeze RADIUS check and
1285 reply attributes to the current values.
1289 #bah, my english is too broken this morning
1290 #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
1291 #the FS::cust_pkg's replace method to trigger the correct export updates when
1292 #package dates change)
1297 $self->{$_} = { $self->$_() }
1298 foreach qw( radius_reply radius_check );
1302 =item forget_snapshot
1304 This methos instructs the object to forget any previously snapshotted
1305 RADIUS check and reply attributes.
1309 sub forget_snapshot {
1313 foreach qw( radius_reply radius_check );
1317 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1319 Returns the domain associated with this account.
1321 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1328 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1329 my $svc_domain = $self->svc_domain(@_)
1330 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1331 $svc_domain->domain;
1336 Returns the FS::svc_domain record for this account's domain (see
1341 # FS::h_svc_acct has a history-aware svc_domain override
1346 ? $self->{'_domsvc'}
1347 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1352 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1356 #inherited from svc_Common
1358 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1360 Returns an email address associated with the account.
1362 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1369 $self->username. '@'. $self->domain(@_);
1374 Returns an array of FS::acct_snarf records associated with the account.
1375 If the acct_snarf table does not exist or there are no associated records,
1376 an empty list is returned
1382 return () unless dbdef->table('acct_snarf');
1383 eval "use FS::acct_snarf;";
1385 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1388 =item decrement_upbytes OCTETS
1390 Decrements the I<upbytes> field of this record by the given amount. If there
1391 is an error, returns the error, otherwise returns false.
1395 sub decrement_upbytes {
1396 shift->_op_usage('-', 'upbytes', @_);
1399 =item increment_upbytes OCTETS
1401 Increments the I<upbytes> field of this record by the given amount. If there
1402 is an error, returns the error, otherwise returns false.
1406 sub increment_upbytes {
1407 shift->_op_usage('+', 'upbytes', @_);
1410 =item decrement_downbytes OCTETS
1412 Decrements the I<downbytes> field of this record by the given amount. If there
1413 is an error, returns the error, otherwise returns false.
1417 sub decrement_downbytes {
1418 shift->_op_usage('-', 'downbytes', @_);
1421 =item increment_downbytes OCTETS
1423 Increments the I<downbytes> field of this record by the given amount. If there
1424 is an error, returns the error, otherwise returns false.
1428 sub increment_downbytes {
1429 shift->_op_usage('+', 'downbytes', @_);
1432 =item decrement_totalbytes OCTETS
1434 Decrements the I<totalbytes> field of this record by the given amount. If there
1435 is an error, returns the error, otherwise returns false.
1439 sub decrement_totalbytes {
1440 shift->_op_usage('-', 'totalbytes', @_);
1443 =item increment_totalbytes OCTETS
1445 Increments the I<totalbytes> field of this record by the given amount. If there
1446 is an error, returns the error, otherwise returns false.
1450 sub increment_totalbytes {
1451 shift->_op_usage('+', 'totalbytes', @_);
1454 =item decrement_seconds SECONDS
1456 Decrements the I<seconds> field of this record by the given amount. If there
1457 is an error, returns the error, otherwise returns false.
1461 sub decrement_seconds {
1462 shift->_op_usage('-', 'seconds', @_);
1465 =item increment_seconds SECONDS
1467 Increments the I<seconds> field of this record by the given amount. If there
1468 is an error, returns the error, otherwise returns false.
1472 sub increment_seconds {
1473 shift->_op_usage('+', 'seconds', @_);
1481 my %op2condition = (
1482 '-' => sub { my($self, $column, $amount) = @_;
1483 $self->$column - $amount <= 0;
1485 '+' => sub { my($self, $column, $amount) = @_;
1486 $self->$column + $amount > 0;
1489 my %op2warncondition = (
1490 '-' => sub { my($self, $column, $amount) = @_;
1491 my $threshold = $column . '_threshold';
1492 $self->$column - $amount <= $self->$threshold + 0;
1494 '+' => sub { my($self, $column, $amount) = @_;
1495 $self->$column + $amount > 0;
1500 my( $self, $op, $column, $amount ) = @_;
1502 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1503 ' ('. $self->email. "): $op $amount\n"
1506 return '' unless $amount;
1508 local $SIG{HUP} = 'IGNORE';
1509 local $SIG{INT} = 'IGNORE';
1510 local $SIG{QUIT} = 'IGNORE';
1511 local $SIG{TERM} = 'IGNORE';
1512 local $SIG{TSTP} = 'IGNORE';
1513 local $SIG{PIPE} = 'IGNORE';
1515 my $oldAutoCommit = $FS::UID::AutoCommit;
1516 local $FS::UID::AutoCommit = 0;
1519 my $sql = "UPDATE svc_acct SET $column = ".
1520 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1521 " $op ? WHERE svcnum = ?";
1525 my $sth = $dbh->prepare( $sql )
1526 or die "Error preparing $sql: ". $dbh->errstr;
1527 my $rv = $sth->execute($amount, $self->svcnum);
1528 die "Error executing $sql: ". $sth->errstr
1529 unless defined($rv);
1530 die "Can't update $column for svcnum". $self->svcnum
1533 my $action = $op2action{$op};
1535 if ( &{$op2condition{$op}}($self, $column, $amount) ) {
1536 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1537 if ($part_export->option('overlimit_groups')) {
1539 my $other = new FS::svc_acct $self->hashref;
1540 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1541 ($self, $part_export->option('overlimit_groups'));
1542 $other->usergroup( $groups );
1543 if ($action eq 'suspend'){
1544 $new = $other; $old = $self;
1546 $new = $self; $old = $other;
1548 my $error = $part_export->export_replace($new, $old);
1550 $dbh->rollback if $oldAutoCommit;
1551 return "Error replacing radius groups in export, ${op}: $error";
1557 if ( $conf->exists("svc_acct-usage_$action")
1558 && &{$op2condition{$op}}($self, $column, $amount) ) {
1559 #my $error = $self->$action();
1560 my $error = $self->cust_svc->cust_pkg->$action();
1562 $dbh->rollback if $oldAutoCommit;
1563 return "Error ${action}ing: $error";
1567 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1568 my $wqueue = new FS::queue {
1569 'svcnum' => $self->svcnum,
1570 'job' => 'FS::svc_acct::reached_threshold',
1575 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1579 my $error = $wqueue->insert(
1580 'svcnum' => $self->svcnum,
1582 'column' => $column,
1586 $dbh->rollback if $oldAutoCommit;
1587 return "Error queuing threshold activity: $error";
1591 warn "$me update successful; committing\n"
1593 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1599 my( $self, $valueref ) = @_;
1601 warn "$me set_usage called for svcnum ". $self->svcnum.
1602 ' ('. $self->email. "): ".
1603 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1606 local $SIG{HUP} = 'IGNORE';
1607 local $SIG{INT} = 'IGNORE';
1608 local $SIG{QUIT} = 'IGNORE';
1609 local $SIG{TERM} = 'IGNORE';
1610 local $SIG{TSTP} = 'IGNORE';
1611 local $SIG{PIPE} = 'IGNORE';
1613 local $FS::svc_Common::noexport_hack = 1;
1614 my $oldAutoCommit = $FS::UID::AutoCommit;
1615 local $FS::UID::AutoCommit = 0;
1620 foreach my $field (keys %$valueref){
1621 $reset = 1 if $valueref->{$field};
1622 $self->setfield($field, $valueref->{$field});
1623 $self->setfield( $field.'_threshold',
1624 int($self->getfield($field)
1625 * ( $conf->exists('svc_acct-usage_threshold')
1626 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1631 $handyhash{$field} = $self->getfield($field);
1632 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1634 #my $error = $self->replace; #NO! we avoid the call to ->check for
1635 #die $error if $error; #services not explicity changed via the UI
1637 my $sql = "UPDATE svc_acct SET " .
1638 join (',', map { "$_ = ?" } (keys %handyhash) ).
1639 " WHERE svcnum = ?";
1644 if (scalar(keys %handyhash)) {
1645 my $sth = $dbh->prepare( $sql )
1646 or die "Error preparing $sql: ". $dbh->errstr;
1647 my $rv = $sth->execute((grep{$_} values %handyhash), $self->svcnum);
1648 die "Error executing $sql: ". $sth->errstr
1649 unless defined($rv);
1650 die "Can't update usage for svcnum ". $self->svcnum
1654 if ( $conf->exists("svc_acct-usage_unsuspend") && $reset ) {
1655 my $error = $self->cust_svc->cust_pkg->unsuspend;
1657 $dbh->rollback if $oldAutoCommit;
1658 return "Error unsuspending: $error";
1662 warn "$me update successful; committing\n"
1664 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1670 =item recharge HASHREF
1672 Increments usage columns by the amount specified in HASHREF as
1673 column=>amount pairs.
1678 my ($self, $vhash) = @_;
1681 warn "[$me] recharge called on $self: ". Dumper($self).
1682 "\nwith vhash: ". Dumper($vhash);
1685 my $oldAutoCommit = $FS::UID::AutoCommit;
1686 local $FS::UID::AutoCommit = 0;
1690 foreach my $column (keys %$vhash){
1691 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1695 $dbh->rollback if $oldAutoCommit;
1697 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1702 =item is_rechargeable
1704 Returns true if this svc_account can be "recharged" and false otherwise.
1708 sub is_rechargable {
1710 $self->seconds ne ''
1711 || $self->upbytes ne ''
1712 || $self->downbytes ne ''
1713 || $self->totalbytes ne '';
1716 =item seconds_since TIMESTAMP
1718 Returns the number of seconds this account has been online since TIMESTAMP,
1719 according to the session monitor (see L<FS::Session>).
1721 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1722 L<Time::Local> and L<Date::Parse> for conversion functions.
1726 #note: POD here, implementation in FS::cust_svc
1729 $self->cust_svc->seconds_since(@_);
1732 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1734 Returns the numbers of seconds this account has been online between
1735 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1736 external SQL radacct table, specified via sqlradius export. Sessions which
1737 started in the specified range but are still open are counted from session
1738 start to the end of the range (unless they are over 1 day old, in which case
1739 they are presumed missing their stop record and not counted). Also, sessions
1740 which end in the range but started earlier are counted from the start of the
1741 range to session end. Finally, sessions which start before the range but end
1742 after are counted for the entire range.
1744 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1745 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1750 #note: POD here, implementation in FS::cust_svc
1751 sub seconds_since_sqlradacct {
1753 $self->cust_svc->seconds_since_sqlradacct(@_);
1756 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1758 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1759 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1760 TIMESTAMP_END (exclusive).
1762 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1763 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1768 #note: POD here, implementation in FS::cust_svc
1769 sub attribute_since_sqlradacct {
1771 $self->cust_svc->attribute_since_sqlradacct(@_);
1774 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1776 Returns an array of hash references of this customers login history for the
1777 given time range. (document this better)
1781 sub get_session_history {
1783 $self->cust_svc->get_session_history(@_);
1786 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
1791 my($self, $start, $end, %opt ) = @_;
1793 my $did = $self->username; #yup
1795 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
1797 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
1799 #SELECT $for_update * FROM cdr
1800 # WHERE calldate >= $start #need a conversion
1801 # AND calldate < $end #ditto
1802 # AND ( charged_party = "$did"
1803 # OR charged_party = "$prefix$did" #if length($prefix);
1804 # OR ( ( charged_party IS NULL OR charged_party = '' )
1806 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
1809 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
1812 if ( length($prefix) ) {
1814 " AND ( charged_party = '$did'
1815 OR charged_party = '$prefix$did'
1816 OR ( ( charged_party IS NULL OR charged_party = '' )
1818 ( src = '$did' OR src = '$prefix$did' )
1824 " AND ( charged_party = '$did'
1825 OR ( ( charged_party IS NULL OR charged_party = '' )
1835 'select' => "$for_update *",
1838 #( freesidestatus IS NULL OR freesidestatus = '' )
1839 'freesidestatus' => '',
1841 'extra_sql' => $charged_or_src,
1849 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1855 if ( $self->usergroup ) {
1856 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
1857 unless ref($self->usergroup) eq 'ARRAY';
1858 #when provisioning records, export callback runs in svc_Common.pm before
1859 #radius_usergroup records can be inserted...
1860 @{$self->usergroup};
1862 map { $_->groupname }
1863 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1867 =item clone_suspended
1869 Constructor used by FS::part_export::_export_suspend fallback. Document
1874 sub clone_suspended {
1876 my %hash = $self->hash;
1877 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1878 new FS::svc_acct \%hash;
1881 =item clone_kludge_unsuspend
1883 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1888 sub clone_kludge_unsuspend {
1890 my %hash = $self->hash;
1891 $hash{_password} = '';
1892 new FS::svc_acct \%hash;
1895 =item check_password
1897 Checks the supplied password against the (possibly encrypted) password in the
1898 database. Returns true for a successful authentication, false for no match.
1900 Currently supported encryptions are: classic DES crypt() and MD5
1904 sub check_password {
1905 my($self, $check_password) = @_;
1907 #remove old-style SUSPENDED kludge, they should be allowed to login to
1908 #self-service and pay up
1909 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1911 #eventually should check a "password-encoding" field
1912 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1914 } elsif ( length($password) < 13 ) { #plaintext
1915 $check_password eq $password;
1916 } elsif ( length($password) == 13 ) { #traditional DES crypt
1917 crypt($check_password, $password) eq $password;
1918 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1919 unix_md5_crypt($check_password, $password) eq $password;
1920 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1921 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1922 $self->svcnum. "\n";
1925 warn "Can't check password: Unrecognized encryption for svcnum ".
1926 $self->svcnum. "\n";
1932 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
1934 Returns an encrypted password, either by passing through an encrypted password
1935 in the database or by encrypting a plaintext password from the database.
1937 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
1938 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
1939 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
1940 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
1941 encryption type is only used if the password is not already encrypted in the
1946 sub crypt_password {
1948 #eventually should check a "password-encoding" field
1949 if ( length($self->_password) == 13
1950 || $self->_password =~ /^\$(1|2a?)\$/
1951 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
1956 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
1957 if ( $encryption eq 'crypt' ) {
1960 $saltset[int(rand(64))].$saltset[int(rand(64))]
1962 } elsif ( $encryption eq 'md5' ) {
1963 unix_md5_crypt( $self->_password );
1964 } elsif ( $encryption eq 'blowfish' ) {
1965 croak "unknown encryption method $encryption";
1967 croak "unknown encryption method $encryption";
1972 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
1974 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
1975 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
1976 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
1978 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
1979 to work the same as the B</crypt_password> method.
1985 #eventually should check a "password-encoding" field
1986 if ( length($self->_password) == 13 ) { #crypt
1987 return '{CRYPT}'. $self->_password;
1988 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
1990 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
1991 die "Blowfish encryption not supported in this context, svcnum ".
1992 $self->svcnum. "\n";
1993 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
1994 return '{SSHA}'. $1;
1995 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
1996 return '{NS-MTA-MD5}'. $1;
1998 return '{PLAIN}'. $self->_password;
1999 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2000 #if ( $encryption eq 'crypt' ) {
2001 # return '{CRYPT}'. crypt(
2003 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2005 #} elsif ( $encryption eq 'md5' ) {
2006 # unix_md5_crypt( $self->_password );
2007 #} elsif ( $encryption eq 'blowfish' ) {
2008 # croak "unknown encryption method $encryption";
2010 # croak "unknown encryption method $encryption";
2015 =item domain_slash_username
2017 Returns $domain/$username/
2021 sub domain_slash_username {
2023 $self->domain. '/'. $self->username. '/';
2026 =item virtual_maildir
2028 Returns $domain/maildirs/$username/
2032 sub virtual_maildir {
2034 $self->domain. '/maildirs/'. $self->username. '/';
2045 This is the FS::svc_acct job-queue-able version. It still uses
2046 FS::Misc::send_email under-the-hood.
2053 eval "use FS::Misc qw(send_email)";
2056 $opt{mimetype} ||= 'text/plain';
2057 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2059 my $error = send_email(
2060 'from' => $opt{from},
2062 'subject' => $opt{subject},
2063 'content-type' => $opt{mimetype},
2064 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2066 die $error if $error;
2069 =item check_and_rebuild_fuzzyfiles
2073 sub check_and_rebuild_fuzzyfiles {
2074 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2075 -e "$dir/svc_acct.username"
2076 or &rebuild_fuzzyfiles;
2079 =item rebuild_fuzzyfiles
2083 sub rebuild_fuzzyfiles {
2085 use Fcntl qw(:flock);
2087 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2091 open(USERNAMELOCK,">>$dir/svc_acct.username")
2092 or die "can't open $dir/svc_acct.username: $!";
2093 flock(USERNAMELOCK,LOCK_EX)
2094 or die "can't lock $dir/svc_acct.username: $!";
2096 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2098 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2099 or die "can't open $dir/svc_acct.username.tmp: $!";
2100 print USERNAMECACHE join("\n", @all_username), "\n";
2101 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2103 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2113 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2114 open(USERNAMECACHE,"<$dir/svc_acct.username")
2115 or die "can't open $dir/svc_acct.username: $!";
2116 my @array = map { chomp; $_; } <USERNAMECACHE>;
2117 close USERNAMECACHE;
2121 =item append_fuzzyfiles USERNAME
2125 sub append_fuzzyfiles {
2126 my $username = shift;
2128 &check_and_rebuild_fuzzyfiles;
2130 use Fcntl qw(:flock);
2132 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2134 open(USERNAME,">>$dir/svc_acct.username")
2135 or die "can't open $dir/svc_acct.username: $!";
2136 flock(USERNAME,LOCK_EX)
2137 or die "can't lock $dir/svc_acct.username: $!";
2139 print USERNAME "$username\n";
2141 flock(USERNAME,LOCK_UN)
2142 or die "can't unlock $dir/svc_acct.username: $!";
2150 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2154 sub radius_usergroup_selector {
2155 my $sel_groups = shift;
2156 my %sel_groups = map { $_=>1 } @$sel_groups;
2158 my $selectname = shift || 'radius_usergroup';
2161 my $sth = $dbh->prepare(
2162 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2163 ) or die $dbh->errstr;
2164 $sth->execute() or die $sth->errstr;
2165 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2169 function ${selectname}_doadd(object) {
2170 var myvalue = object.${selectname}_add.value;
2171 var optionName = new Option(myvalue,myvalue,false,true);
2172 var length = object.$selectname.length;
2173 object.$selectname.options[length] = optionName;
2174 object.${selectname}_add.value = "";
2177 <SELECT MULTIPLE NAME="$selectname">
2180 foreach my $group ( @all_groups ) {
2181 $html .= qq(<OPTION VALUE="$group");
2182 if ( $sel_groups{$group} ) {
2183 $html .= ' SELECTED';
2184 $sel_groups{$group} = 0;
2186 $html .= ">$group</OPTION>\n";
2188 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2189 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2191 $html .= '</SELECT>';
2193 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2194 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2199 =item reached_threshold
2201 Performs some activities when svc_acct thresholds (such as number of seconds
2202 remaining) are reached.
2206 sub reached_threshold {
2209 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2210 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2212 if ( $opt{'op'} eq '+' ){
2213 $svc_acct->setfield( $opt{'column'}.'_threshold',
2214 int($svc_acct->getfield($opt{'column'})
2215 * ( $conf->exists('svc_acct-usage_threshold')
2216 ? $conf->config('svc_acct-usage_threshold')/100
2221 my $error = $svc_acct->replace;
2222 die $error if $error;
2223 }elsif ( $opt{'op'} eq '-' ){
2225 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2226 return '' if ($threshold eq '' );
2228 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2229 my $error = $svc_acct->replace;
2230 die $error if $error; # email next time, i guess
2232 if ( $warning_template ) {
2233 eval "use FS::Misc qw(send_email)";
2236 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2237 my $cust_main = $cust_pkg->cust_main;
2239 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2240 $cust_main->invoicing_list,
2242 ($opt{'to'} ? $opt{'to'} : ())
2245 my $mimetype = $warning_mimetype;
2246 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2248 my $body = $warning_template->fill_in( HASH => {
2249 'custnum' => $cust_main->custnum,
2250 'username' => $svc_acct->username,
2251 'password' => $svc_acct->_password,
2252 'first' => $cust_main->first,
2253 'last' => $cust_main->getfield('last'),
2254 'pkg' => $cust_pkg->part_pkg->pkg,
2255 'column' => $opt{'column'},
2256 'amount' => $svc_acct->getfield($opt{'column'}),
2257 'threshold' => $threshold,
2261 my $error = send_email(
2262 'from' => $warning_from,
2264 'subject' => $warning_subject,
2265 'content-type' => $mimetype,
2266 'body' => [ map "$_\n", split("\n", $body) ],
2268 die $error if $error;
2271 die "unknown op: " . $opt{'op'};
2279 The $recref stuff in sub check should be cleaned up.
2281 The suspend, unsuspend and cancel methods update the database, but not the
2282 current object. This is probably a bug as it's unexpected and
2285 radius_usergroup_selector? putting web ui components in here? they should
2286 probably live somewhere else...
2288 insertion of RADIUS group stuff in insert could be done with child_objects now
2289 (would probably clean up export of them too)
2293 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2294 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2295 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2296 L<freeside-queued>), L<FS::svc_acct_pop>,
2297 schema.html from the base documentation.