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 $welcome_subject $welcome_mimetype
13 $radius_password $radius_ip
19 use Crypt::PasswdMD5 1.2;
20 use FS::UID qw( datasrc );
22 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
27 use FS::cust_main_invoice;
31 use FS::radius_usergroup;
34 use FS::Msgcat qw(gettext);
38 @ISA = qw( FS::svc_Common );
41 $me = '[FS::svc_acct]';
43 #ask FS::UID to run this stuff for us later
44 $FS::UID::callback{'FS::svc_acct'} = sub {
46 $dir_prefix = $conf->config('home');
47 @shells = $conf->config('shells');
48 $usernamemin = $conf->config('usernamemin') || 2;
49 $usernamemax = $conf->config('usernamemax');
50 $passwordmin = $conf->config('passwordmin') || 6;
51 $passwordmax = $conf->config('passwordmax') || 8;
52 $username_letter = $conf->exists('username-letter');
53 $username_letterfirst = $conf->exists('username-letterfirst');
54 $username_noperiod = $conf->exists('username-noperiod');
55 $username_nounderscore = $conf->exists('username-nounderscore');
56 $username_nodash = $conf->exists('username-nodash');
57 $username_uppercase = $conf->exists('username-uppercase');
58 $username_ampersand = $conf->exists('username-ampersand');
59 $username_percent = $conf->exists('username-percent');
60 $password_noampersand = $conf->exists('password-noexclamation');
61 $password_noexclamation = $conf->exists('password-noexclamation');
62 $dirhash = $conf->config('dirhash') || 0;
63 if ( $conf->exists('welcome_email') ) {
64 $welcome_template = new Text::Template (
66 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
67 ) or warn "can't create welcome email template: $Text::Template::ERROR";
68 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
69 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
70 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
72 $welcome_template = '';
74 $welcome_subject = '';
75 $welcome_mimetype = '';
77 $smtpmachine = $conf->config('smtpmachine');
78 $radius_password = $conf->config('radius-password') || 'Password';
79 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
82 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
83 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
87 my ( $hashref, $cache ) = @_;
88 if ( $hashref->{'svc_acct_svcnum'} ) {
89 $self->{'_domsvc'} = FS::svc_domain->new( {
90 'svcnum' => $hashref->{'domsvc'},
91 'domain' => $hashref->{'svc_acct_domain'},
92 'catchall' => $hashref->{'svc_acct_catchall'},
99 FS::svc_acct - Object methods for svc_acct records
105 $record = new FS::svc_acct \%hash;
106 $record = new FS::svc_acct { 'column' => 'value' };
108 $error = $record->insert;
110 $error = $new_record->replace($old_record);
112 $error = $record->delete;
114 $error = $record->check;
116 $error = $record->suspend;
118 $error = $record->unsuspend;
120 $error = $record->cancel;
122 %hash = $record->radius;
124 %hash = $record->radius_reply;
126 %hash = $record->radius_check;
128 $domain = $record->domain;
130 $svc_domain = $record->svc_domain;
132 $email = $record->email;
134 $seconds_since = $record->seconds_since($timestamp);
138 An FS::svc_acct object represents an account. FS::svc_acct inherits from
139 FS::svc_Common. The following fields are currently supported:
143 =item svcnum - primary key (assigned automatcially for new accounts)
147 =item _password - generated if blank
149 =item sec_phrase - security phrase
151 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
159 =item dir - set automatically if blank (and uid is not)
163 =item quota - (unimplementd)
165 =item slipip - IP address
169 =item domsvc - svcnum from svc_domain
171 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
173 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
183 Creates a new account. To add the account to the database, see L<"insert">.
187 sub table { 'svc_acct'; }
189 =item insert [ , OPTION => VALUE ... ]
191 Adds this account to the database. If there is an error, returns the error,
192 otherwise returns false.
194 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
195 defined. An FS::cust_svc record will be created and inserted.
197 The additional field I<usergroup> can optionally be defined; if so it should
198 contain an arrayref of group names. See L<FS::radius_usergroup>.
200 The additional field I<child_objects> can optionally be defined; if so it
201 should contain an arrayref of FS::tablename objects. They will have their
202 svcnum fields set and will be inserted after this record, but before any
203 exports are run. Each element of the array can also optionally be a
204 two-element array reference containing the child object and the name of an
205 alternate field to be filled in with the newly-inserted svcnum, for example
206 C<[ $svc_forward, 'srcsvc' ]>
208 Currently available options are: I<depend_jobnum>
210 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
211 jobnums), all provisioning jobs will have a dependancy on the supplied
212 jobnum(s) (they will not run until the specific job(s) complete(s)).
214 (TODOC: L<FS::queue> and L<freeside-queued>)
216 (TODOC: new exports!)
225 local $SIG{HUP} = 'IGNORE';
226 local $SIG{INT} = 'IGNORE';
227 local $SIG{QUIT} = 'IGNORE';
228 local $SIG{TERM} = 'IGNORE';
229 local $SIG{TSTP} = 'IGNORE';
230 local $SIG{PIPE} = 'IGNORE';
232 my $oldAutoCommit = $FS::UID::AutoCommit;
233 local $FS::UID::AutoCommit = 0;
236 $error = $self->check;
237 return $error if $error;
239 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
240 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
241 unless ( $cust_svc ) {
242 $dbh->rollback if $oldAutoCommit;
243 return "no cust_svc record found for svcnum ". $self->svcnum;
245 $self->pkgnum($cust_svc->pkgnum);
246 $self->svcpart($cust_svc->svcpart);
249 $error = $self->_check_duplicate;
251 $dbh->rollback if $oldAutoCommit;
256 $error = $self->SUPER::insert(
257 'jobnums' => \@jobnums,
258 'child_objects' => $self->child_objects,
262 $dbh->rollback if $oldAutoCommit;
266 if ( $self->usergroup ) {
267 foreach my $groupname ( @{$self->usergroup} ) {
268 my $radius_usergroup = new FS::radius_usergroup ( {
269 svcnum => $self->svcnum,
270 groupname => $groupname,
272 my $error = $radius_usergroup->insert;
274 $dbh->rollback if $oldAutoCommit;
280 unless ( $skip_fuzzyfiles ) {
281 $error = $self->queue_fuzzyfiles_update;
283 $dbh->rollback if $oldAutoCommit;
284 return "updating fuzzy search cache: $error";
288 my $cust_pkg = $self->cust_svc->cust_pkg;
291 my $cust_main = $cust_pkg->cust_main;
293 if ( $conf->exists('emailinvoiceauto') ) {
294 my @invoicing_list = $cust_main->invoicing_list;
295 push @invoicing_list, $self->email;
296 $cust_main->invoicing_list(\@invoicing_list);
301 if ( $welcome_template && $cust_pkg ) {
302 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
304 my $wqueue = new FS::queue {
305 'svcnum' => $self->svcnum,
306 'job' => 'FS::svc_acct::send_email'
308 my $error = $wqueue->insert(
310 'from' => $welcome_from,
311 'subject' => $welcome_subject,
312 'mimetype' => $welcome_mimetype,
313 'body' => $welcome_template->fill_in( HASH => {
314 'custnum' => $self->custnum,
315 'username' => $self->username,
316 'password' => $self->_password,
317 'first' => $cust_main->first,
318 'last' => $cust_main->getfield('last'),
319 'pkg' => $cust_pkg->part_pkg->pkg,
323 $dbh->rollback if $oldAutoCommit;
324 return "error queuing welcome email: $error";
327 if ( $options{'depend_jobnum'} ) {
328 warn "$me depend_jobnum found; adding to welcome email dependancies"
330 if ( ref($options{'depend_jobnum'}) ) {
331 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
332 "to welcome email dependancies"
334 push @jobnums, @{ $options{'depend_jobnum'} };
336 warn "$me adding job $options{'depend_jobnum'} ".
337 "to welcome email dependancies"
339 push @jobnums, $options{'depend_jobnum'};
343 foreach my $jobnum ( @jobnums ) {
344 my $error = $wqueue->depend_insert($jobnum);
346 $dbh->rollback if $oldAutoCommit;
347 return "error queuing welcome email job dependancy: $error";
357 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
363 Deletes this account from the database. If there is an error, returns the
364 error, otherwise returns false.
366 The corresponding FS::cust_svc record will be deleted as well.
368 (TODOC: new exports!)
375 return "can't delete system account" if $self->_check_system;
377 return "Can't delete an account which is a (svc_forward) source!"
378 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
380 return "Can't delete an account which is a (svc_forward) destination!"
381 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
383 return "Can't delete an account with (svc_www) web service!"
384 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
386 # what about records in session ? (they should refer to history table)
388 local $SIG{HUP} = 'IGNORE';
389 local $SIG{INT} = 'IGNORE';
390 local $SIG{QUIT} = 'IGNORE';
391 local $SIG{TERM} = 'IGNORE';
392 local $SIG{TSTP} = 'IGNORE';
393 local $SIG{PIPE} = 'IGNORE';
395 my $oldAutoCommit = $FS::UID::AutoCommit;
396 local $FS::UID::AutoCommit = 0;
399 foreach my $cust_main_invoice (
400 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
402 unless ( defined($cust_main_invoice) ) {
403 warn "WARNING: something's wrong with qsearch";
406 my %hash = $cust_main_invoice->hash;
407 $hash{'dest'} = $self->email;
408 my $new = new FS::cust_main_invoice \%hash;
409 my $error = $new->replace($cust_main_invoice);
411 $dbh->rollback if $oldAutoCommit;
416 foreach my $svc_domain (
417 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
419 my %hash = new FS::svc_domain->hash;
420 $hash{'catchall'} = '';
421 my $new = new FS::svc_domain \%hash;
422 my $error = $new->replace($svc_domain);
424 $dbh->rollback if $oldAutoCommit;
429 foreach my $radius_usergroup (
430 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
432 my $error = $radius_usergroup->delete;
434 $dbh->rollback if $oldAutoCommit;
439 my $error = $self->SUPER::delete;
441 $dbh->rollback if $oldAutoCommit;
445 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
449 =item replace OLD_RECORD
451 Replaces OLD_RECORD with this one in the database. If there is an error,
452 returns the error, otherwise returns false.
454 The additional field I<usergroup> can optionally be defined; if so it should
455 contain an arrayref of group names. See L<FS::radius_usergroup>.
461 my ( $new, $old ) = ( shift, shift );
463 warn "$me replacing $old with $new\n" if $DEBUG;
465 return "can't modify system account" if $old->_check_system;
468 #no warnings 'numeric'; #alas, a 5.006-ism
471 foreach my $xid (qw( uid gid )) {
473 return "Can't change $xid!"
474 if ! $conf->exists("svc_acct-edit_$xid")
475 && $old->$xid() != $new->$xid()
476 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
481 #change homdir when we change username
482 $new->setfield('dir', '') if $old->username ne $new->username;
484 local $SIG{HUP} = 'IGNORE';
485 local $SIG{INT} = 'IGNORE';
486 local $SIG{QUIT} = 'IGNORE';
487 local $SIG{TERM} = 'IGNORE';
488 local $SIG{TSTP} = 'IGNORE';
489 local $SIG{PIPE} = 'IGNORE';
491 my $oldAutoCommit = $FS::UID::AutoCommit;
492 local $FS::UID::AutoCommit = 0;
495 # redundant, but so $new->usergroup gets set
496 $error = $new->check;
497 return $error if $error;
499 $old->usergroup( [ $old->radius_groups ] );
501 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
502 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
504 if ( $new->usergroup ) {
505 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
506 my @newgroups = @{$new->usergroup};
507 foreach my $oldgroup ( @{$old->usergroup} ) {
508 if ( grep { $oldgroup eq $_ } @newgroups ) {
509 @newgroups = grep { $oldgroup ne $_ } @newgroups;
512 my $radius_usergroup = qsearchs('radius_usergroup', {
513 svcnum => $old->svcnum,
514 groupname => $oldgroup,
516 my $error = $radius_usergroup->delete;
518 $dbh->rollback if $oldAutoCommit;
519 return "error deleting radius_usergroup $oldgroup: $error";
523 foreach my $newgroup ( @newgroups ) {
524 my $radius_usergroup = new FS::radius_usergroup ( {
525 svcnum => $new->svcnum,
526 groupname => $newgroup,
528 my $error = $radius_usergroup->insert;
530 $dbh->rollback if $oldAutoCommit;
531 return "error adding radius_usergroup $newgroup: $error";
537 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
538 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
539 $error = $new->_check_duplicate;
541 $dbh->rollback if $oldAutoCommit;
546 $error = $new->SUPER::replace($old);
548 $dbh->rollback if $oldAutoCommit;
549 return $error if $error;
552 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
553 $error = $new->queue_fuzzyfiles_update;
555 $dbh->rollback if $oldAutoCommit;
556 return "updating fuzzy search cache: $error";
560 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
564 =item queue_fuzzyfiles_update
566 Used by insert & replace to update the fuzzy search cache
570 sub queue_fuzzyfiles_update {
573 local $SIG{HUP} = 'IGNORE';
574 local $SIG{INT} = 'IGNORE';
575 local $SIG{QUIT} = 'IGNORE';
576 local $SIG{TERM} = 'IGNORE';
577 local $SIG{TSTP} = 'IGNORE';
578 local $SIG{PIPE} = 'IGNORE';
580 my $oldAutoCommit = $FS::UID::AutoCommit;
581 local $FS::UID::AutoCommit = 0;
584 my $queue = new FS::queue {
585 'svcnum' => $self->svcnum,
586 'job' => 'FS::svc_acct::append_fuzzyfiles'
588 my $error = $queue->insert($self->username);
590 $dbh->rollback if $oldAutoCommit;
591 return "queueing job (transaction rolled back): $error";
594 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
602 Suspends this account by calling export-specific suspend hooks. If there is
603 an error, returns the error, otherwise returns false.
605 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
611 return "can't suspend system account" if $self->_check_system;
612 $self->SUPER::suspend;
617 Unsuspends this account by by calling export-specific suspend hooks. If there
618 is an error, returns the error, otherwise returns false.
620 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
626 my %hash = $self->hash;
627 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
628 $hash{_password} = $1;
629 my $new = new FS::svc_acct ( \%hash );
630 my $error = $new->replace($self);
631 return $error if $error;
634 $self->SUPER::unsuspend;
639 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
641 If the B<auto_unset_catchall> configuration option is set, this method will
642 automatically remove any references to the canceled service in the catchall
643 field of svc_domain. This allows packages that contain both a svc_domain and
644 its catchall svc_acct to be canceled in one step.
649 # Only one thing to do at this level
651 foreach my $svc_domain (
652 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
653 if($conf->exists('auto_unset_catchall')) {
654 my %hash = $svc_domain->hash;
655 $hash{catchall} = '';
656 my $new = new FS::svc_domain ( \%hash );
657 my $error = $new->replace($svc_domain);
658 return $error if $error;
660 return "cannot unprovision svc_acct #".$self->svcnum.
661 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
665 $self->SUPER::cancel;
671 Checks all fields to make sure this is a valid service. If there is an error,
672 returns the error, otherwise returns false. Called by the insert and replace
675 Sets any fixed values; see L<FS::part_svc>.
682 my($recref) = $self->hashref;
684 my $x = $self->setfixed;
685 return $x unless ref($x);
688 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
690 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
693 my $error = $self->ut_numbern('svcnum')
694 #|| $self->ut_number('domsvc')
695 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
696 || $self->ut_textn('sec_phrase')
698 return $error if $error;
700 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
701 if ( $username_uppercase ) {
702 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
703 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
704 $recref->{username} = $1;
706 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/
707 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
708 $recref->{username} = $1;
711 if ( $username_letterfirst ) {
712 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
713 } elsif ( $username_letter ) {
714 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
716 if ( $username_noperiod ) {
717 $recref->{username} =~ /\./ and return gettext('illegal_username');
719 if ( $username_nounderscore ) {
720 $recref->{username} =~ /_/ and return gettext('illegal_username');
722 if ( $username_nodash ) {
723 $recref->{username} =~ /\-/ and return gettext('illegal_username');
725 unless ( $username_ampersand ) {
726 $recref->{username} =~ /\&/ and return gettext('illegal_username');
728 if ( $password_noampersand ) {
729 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
731 if ( $password_noexclamation ) {
732 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
734 unless ( $username_percent ) {
735 $recref->{username} =~ /\%/ and return gettext('illegal_username');
738 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
739 $recref->{popnum} = $1;
740 return "Unknown popnum" unless
741 ! $recref->{popnum} ||
742 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
744 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
746 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
747 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
749 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
750 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
751 #not all systems use gid=uid
752 #you can set a fixed gid in part_svc
754 return "Only root can have uid 0"
755 if $recref->{uid} == 0
756 && $recref->{username} !~ /^(root|toor|smtp)$/;
758 unless ( $recref->{username} eq 'sync' ) {
759 if ( grep $_ eq $recref->{shell}, @shells ) {
760 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
762 return "Illegal shell \`". $self->shell. "\'; ".
763 $conf->dir. "/shells contains: @shells";
766 $recref->{shell} = '/bin/sync';
770 $recref->{gid} ne '' ?
771 return "Can't have gid without uid" : ( $recref->{gid}='' );
772 #$recref->{dir} ne '' ?
773 # return "Can't have directory without uid" : ( $recref->{dir}='' );
774 $recref->{shell} ne '' ?
775 return "Can't have shell without uid" : ( $recref->{shell}='' );
778 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
780 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
781 or return "Illegal directory: ". $recref->{dir};
783 return "Illegal directory"
784 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
785 return "Illegal directory"
786 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
787 unless ( $recref->{dir} ) {
788 $recref->{dir} = $dir_prefix . '/';
789 if ( $dirhash > 0 ) {
790 for my $h ( 1 .. $dirhash ) {
791 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
793 } elsif ( $dirhash < 0 ) {
794 for my $h ( reverse $dirhash .. -1 ) {
795 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
798 $recref->{dir} .= $recref->{username};
804 # $error = $self->ut_textn('finger');
805 # return $error if $error;
806 if ( $self->getfield('finger') eq '' ) {
807 my $cust_pkg = $self->svcnum
808 ? $self->cust_svc->cust_pkg
809 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
811 my $cust_main = $cust_pkg->cust_main;
812 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
815 $self->getfield('finger') =~
816 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
817 or return "Illegal finger: ". $self->getfield('finger');
818 $self->setfield('finger', $1);
820 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
821 $recref->{quota} = $1;
823 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
824 if ( $recref->{slipip} eq '' ) {
825 $recref->{slipip} = '';
826 } elsif ( $recref->{slipip} eq '0e0' ) {
827 $recref->{slipip} = '0e0';
829 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
830 or return "Illegal slipip: ". $self->slipip;
831 $recref->{slipip} = $1;
836 #arbitrary RADIUS stuff; allow ut_textn for now
837 foreach ( grep /^radius_/, fields('svc_acct') ) {
841 #generate a password if it is blank
842 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
843 unless ( $recref->{_password} );
845 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
846 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
847 $recref->{_password} = $1.$3;
848 #uncomment this to encrypt password immediately upon entry, or run
849 #bin/crypt_pw in cron to give new users a window during which their
850 #password is available to techs, for faxing, etc. (also be aware of
852 #$recref->{password} = $1.
853 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
855 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
856 $recref->{_password} = $1.$3;
857 } elsif ( $recref->{_password} eq '*' ) {
858 $recref->{_password} = '*';
859 } elsif ( $recref->{_password} eq '!' ) {
860 $recref->{_password} = '!';
861 } elsif ( $recref->{_password} eq '!!' ) {
862 $recref->{_password} = '!!';
864 #return "Illegal password";
865 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
866 FS::Msgcat::_gettext('illegal_password_characters').
867 ": ". $recref->{_password};
875 Internal function to check the username against the list of system usernames
876 from the I<system_usernames> configuration value. Returns true if the username
877 is listed on the system username list.
883 scalar( grep { $self->username eq $_ || $self->email eq $_ }
884 $conf->config('system_usernames')
888 =item _check_duplicate
890 Internal function to check for duplicates usernames, username@domain pairs and
893 If the I<global_unique-username> configuration value is set to B<username> or
894 B<username@domain>, enforces global username or username@domain uniqueness.
896 In all cases, check for duplicate uids and usernames or username@domain pairs
897 per export and with identical I<svcpart> values.
901 sub _check_duplicate {
904 #this is Pg-specific. what to do for mysql etc?
905 # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
906 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
907 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
909 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
911 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
912 unless ( $part_svc ) {
913 return 'unknown svcpart '. $self->svcpart;
916 my $global_unique = $conf->config('global_unique-username') || 'none';
918 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
919 qsearch( 'svc_acct', { 'username' => $self->username } );
920 return gettext('username_in_use')
921 if $global_unique eq 'username' && @dup_user;
923 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
924 qsearch( 'svc_acct', { 'username' => $self->username,
925 'domsvc' => $self->domsvc } );
926 return gettext('username_in_use')
927 if $global_unique eq 'username@domain' && @dup_userdomain;
930 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
931 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
932 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
933 qsearch( 'svc_acct', { 'uid' => $self->uid } );
938 if ( @dup_user || @dup_userdomain || @dup_uid ) {
939 my $exports = FS::part_export::export_info('svc_acct');
940 my %conflict_user_svcpart;
941 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
943 foreach my $part_export ( $part_svc->part_export ) {
945 #this will catch to the same exact export
946 my @svcparts = map { $_->svcpart } $part_export->export_svc;
948 #this will catch to exports w/same exporthost+type ???
949 #my @other_part_export = qsearch('part_export', {
950 # 'machine' => $part_export->machine,
951 # 'exporttype' => $part_export->exporttype,
953 #foreach my $other_part_export ( @other_part_export ) {
954 # push @svcparts, map { $_->svcpart }
955 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
958 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
959 #silly kludge to avoid uninitialized value errors
960 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
961 ? $exports->{$part_export->exporttype}{'nodomain'}
963 if ( $nodomain =~ /^Y/i ) {
964 $conflict_user_svcpart{$_} = $part_export->exportnum
967 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
972 foreach my $dup_user ( @dup_user ) {
973 my $dup_svcpart = $dup_user->cust_svc->svcpart;
974 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
975 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
976 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
980 foreach my $dup_userdomain ( @dup_userdomain ) {
981 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
982 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
983 return "duplicate username\@domain: conflicts with svcnum ".
984 $dup_userdomain->svcnum. " via exportnum ".
985 $conflict_userdomain_svcpart{$dup_svcpart};
989 foreach my $dup_uid ( @dup_uid ) {
990 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
991 if ( exists($conflict_user_svcpart{$dup_svcpart})
992 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
993 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
994 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
995 || $conflict_userdomain_svcpart{$dup_svcpart};
1007 Depriciated, use radius_reply instead.
1012 carp "FS::svc_acct::radius depriciated, use radius_reply";
1013 $_[0]->radius_reply;
1018 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1019 reply attributes of this record.
1021 Note that this is now the preferred method for reading RADIUS attributes -
1022 accessing the columns directly is discouraged, as the column names are
1023 expected to change in the future.
1030 return %{ $self->{'radius_reply'} }
1031 if exists $self->{'radius_reply'};
1036 my($column, $attrib) = ($1, $2);
1037 #$attrib =~ s/_/\-/g;
1038 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1039 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1041 if ( $self->slipip && $self->slipip ne '0e0' ) {
1042 $reply{$radius_ip} = $self->slipip;
1045 if ( $self->seconds !~ /^$/ ) {
1046 $reply{'Session-Timeout'} = $self->seconds;
1054 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1055 check attributes of this record.
1057 Note that this is now the preferred method for reading RADIUS attributes -
1058 accessing the columns directly is discouraged, as the column names are
1059 expected to change in the future.
1066 return %{ $self->{'radius_check'} }
1067 if exists $self->{'radius_check'};
1072 my($column, $attrib) = ($1, $2);
1073 #$attrib =~ s/_/\-/g;
1074 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1075 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1077 my $password = $self->_password;
1078 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1080 my $cust_svc = $self->cust_svc;
1081 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1083 my $cust_pkg = $cust_svc->cust_pkg;
1084 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1085 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1094 This method instructs the object to "snapshot" or freeze RADIUS check and
1095 reply attributes to the current values.
1099 #bah, my english is too broken this morning
1100 #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
1101 #the FS::cust_pkg's replace method to trigger the correct export updates when
1102 #package dates change)
1107 $self->{$_} = { $self->$_() }
1108 foreach qw( radius_reply radius_check );
1112 =item forget_snapshot
1114 This methos instructs the object to forget any previously snapshotted
1115 RADIUS check and reply attributes.
1119 sub forget_snapshot {
1123 foreach qw( radius_reply radius_check );
1129 Returns the domain associated with this account.
1135 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1136 my $svc_domain = $self->svc_domain(@_)
1137 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1138 $svc_domain->domain;
1143 Returns the FS::svc_domain record for this account's domain (see
1151 ? $self->{'_domsvc'}
1152 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1157 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1161 #inherited from svc_Common
1165 Returns an email address associated with the account.
1171 $self->username. '@'. $self->domain(@_);
1176 Returns an array of FS::acct_snarf records associated with the account.
1177 If the acct_snarf table does not exist or there are no associated records,
1178 an empty list is returned
1184 return () unless dbdef->table('acct_snarf');
1185 eval "use FS::acct_snarf;";
1187 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1190 =item decrement_seconds SECONDS
1192 Decrements the I<seconds> field of this record by the given amount. If there
1193 is an error, returns the error, otherwise returns false.
1197 sub decrement_seconds {
1198 shift->_op_seconds('-', @_);
1201 =item increment_seconds SECONDS
1203 Increments the I<seconds> field of this record by the given amount. If there
1204 is an error, returns the error, otherwise returns false.
1208 sub increment_seconds {
1209 shift->_op_seconds('+', @_);
1217 my %op2condition = (
1218 '-' => sub { my($self, $seconds) = @_;
1219 $self->seconds - $seconds <= 0;
1221 '+' => sub { my($self, $seconds) = @_;
1222 $self->seconds + $seconds > 0;
1227 my( $self, $op, $seconds ) = @_;
1228 warn "$me _op_seconds called for svcnum ". $self->svcnum.
1229 ' ('. $self->email. "): $op $seconds\n"
1232 local $SIG{HUP} = 'IGNORE';
1233 local $SIG{INT} = 'IGNORE';
1234 local $SIG{QUIT} = 'IGNORE';
1235 local $SIG{TERM} = 'IGNORE';
1236 local $SIG{TSTP} = 'IGNORE';
1237 local $SIG{PIPE} = 'IGNORE';
1239 my $oldAutoCommit = $FS::UID::AutoCommit;
1240 local $FS::UID::AutoCommit = 0;
1243 my $sql = "UPDATE svc_acct SET seconds = ".
1244 " CASE WHEN seconds IS NULL THEN 0 ELSE seconds END ". #$seconds||0
1245 " $op ? WHERE svcnum = ?";
1249 my $sth = $dbh->prepare( $sql )
1250 or die "Error preparing $sql: ". $dbh->errstr;
1251 my $rv = $sth->execute($seconds, $self->svcnum);
1252 die "Error executing $sql: ". $sth->errstr
1253 unless defined($rv);
1254 die "Can't update seconds for svcnum". $self->svcnum
1257 my $action = $op2action{$op};
1259 if ( $conf->exists("svc_acct-usage_$action")
1260 && &{$op2condition{$op}}($self, $seconds) ) {
1261 #my $error = $self->$action();
1262 my $error = $self->cust_svc->cust_pkg->$action();
1264 $dbh->rollback if $oldAutoCommit;
1265 return "Error ${action}ing: $error";
1269 warn "$me update sucessful; committing\n"
1271 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1277 =item seconds_since TIMESTAMP
1279 Returns the number of seconds this account has been online since TIMESTAMP,
1280 according to the session monitor (see L<FS::Session>).
1282 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1283 L<Time::Local> and L<Date::Parse> for conversion functions.
1287 #note: POD here, implementation in FS::cust_svc
1290 $self->cust_svc->seconds_since(@_);
1293 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1295 Returns the numbers of seconds this account has been online between
1296 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1297 external SQL radacct table, specified via sqlradius export. Sessions which
1298 started in the specified range but are still open are counted from session
1299 start to the end of the range (unless they are over 1 day old, in which case
1300 they are presumed missing their stop record and not counted). Also, sessions
1301 which end in the range but started earlier are counted from the start of the
1302 range to session end. Finally, sessions which start before the range but end
1303 after are counted for the entire range.
1305 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1306 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1311 #note: POD here, implementation in FS::cust_svc
1312 sub seconds_since_sqlradacct {
1314 $self->cust_svc->seconds_since_sqlradacct(@_);
1317 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1319 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1320 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1321 TIMESTAMP_END (exclusive).
1323 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1324 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1329 #note: POD here, implementation in FS::cust_svc
1330 sub attribute_since_sqlradacct {
1332 $self->cust_svc->attribute_since_sqlradacct(@_);
1335 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1337 Returns an array of hash references of this customers login history for the
1338 given time range. (document this better)
1342 sub get_session_history {
1344 $self->cust_svc->get_session_history(@_);
1349 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1355 if ( $self->usergroup ) {
1356 #when provisioning records, export callback runs in svc_Common.pm before
1357 #radius_usergroup records can be inserted...
1358 @{$self->usergroup};
1360 map { $_->groupname }
1361 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1365 =item clone_suspended
1367 Constructor used by FS::part_export::_export_suspend fallback. Document
1372 sub clone_suspended {
1374 my %hash = $self->hash;
1375 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1376 new FS::svc_acct \%hash;
1379 =item clone_kludge_unsuspend
1381 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1386 sub clone_kludge_unsuspend {
1388 my %hash = $self->hash;
1389 $hash{_password} = '';
1390 new FS::svc_acct \%hash;
1393 =item check_password
1395 Checks the supplied password against the (possibly encrypted) password in the
1396 database. Returns true for a sucessful authentication, false for no match.
1398 Currently supported encryptions are: classic DES crypt() and MD5
1402 sub check_password {
1403 my($self, $check_password) = @_;
1405 #remove old-style SUSPENDED kludge, they should be allowed to login to
1406 #self-service and pay up
1407 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1409 #eventually should check a "password-encoding" field
1410 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1412 } elsif ( length($password) < 13 ) { #plaintext
1413 $check_password eq $password;
1414 } elsif ( length($password) == 13 ) { #traditional DES crypt
1415 crypt($check_password, $password) eq $password;
1416 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1417 unix_md5_crypt($check_password, $password) eq $password;
1418 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1419 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1420 $self->svcnum. "\n";
1423 warn "Can't check password: Unrecognized encryption for svcnum ".
1424 $self->svcnum. "\n";
1430 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
1432 Returns an encrypted password, either by passing through an encrypted password
1433 in the database or by encrypting a plaintext password from the database.
1435 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
1436 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
1437 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
1438 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
1439 encryption type is only used if the password is not already encrypted in the
1444 sub crypt_password {
1446 #eventually should check a "password-encoding" field
1447 if ( length($self->_password) == 13
1448 || $self->_password =~ /^\$(1|2a?)\$/
1449 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
1454 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
1455 if ( $encryption eq 'crypt' ) {
1458 $saltset[int(rand(64))].$saltset[int(rand(64))]
1460 } elsif ( $encryption eq 'md5' ) {
1461 unix_md5_crypt( $self->_password );
1462 } elsif ( $encryption eq 'blowfish' ) {
1463 die "unknown encryption method $encryption";
1465 die "unknown encryption method $encryption";
1470 =item virtual_maildir
1472 Returns $domain/maildirs/$username/
1476 sub virtual_maildir {
1478 $self->domain. '/maildirs/'. $self->username. '/';
1489 This is the FS::svc_acct job-queue-able version. It still uses
1490 FS::Misc::send_email under-the-hood.
1497 eval "use FS::Misc qw(send_email)";
1500 $opt{mimetype} ||= 'text/plain';
1501 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1503 my $error = send_email(
1504 'from' => $opt{from},
1506 'subject' => $opt{subject},
1507 'content-type' => $opt{mimetype},
1508 'body' => [ map "$_\n", split("\n", $opt{body}) ],
1510 die $error if $error;
1513 =item check_and_rebuild_fuzzyfiles
1517 sub check_and_rebuild_fuzzyfiles {
1518 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1519 -e "$dir/svc_acct.username"
1520 or &rebuild_fuzzyfiles;
1523 =item rebuild_fuzzyfiles
1527 sub rebuild_fuzzyfiles {
1529 use Fcntl qw(:flock);
1531 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1535 open(USERNAMELOCK,">>$dir/svc_acct.username")
1536 or die "can't open $dir/svc_acct.username: $!";
1537 flock(USERNAMELOCK,LOCK_EX)
1538 or die "can't lock $dir/svc_acct.username: $!";
1540 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1542 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1543 or die "can't open $dir/svc_acct.username.tmp: $!";
1544 print USERNAMECACHE join("\n", @all_username), "\n";
1545 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1547 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1557 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1558 open(USERNAMECACHE,"<$dir/svc_acct.username")
1559 or die "can't open $dir/svc_acct.username: $!";
1560 my @array = map { chomp; $_; } <USERNAMECACHE>;
1561 close USERNAMECACHE;
1565 =item append_fuzzyfiles USERNAME
1569 sub append_fuzzyfiles {
1570 my $username = shift;
1572 &check_and_rebuild_fuzzyfiles;
1574 use Fcntl qw(:flock);
1576 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1578 open(USERNAME,">>$dir/svc_acct.username")
1579 or die "can't open $dir/svc_acct.username: $!";
1580 flock(USERNAME,LOCK_EX)
1581 or die "can't lock $dir/svc_acct.username: $!";
1583 print USERNAME "$username\n";
1585 flock(USERNAME,LOCK_UN)
1586 or die "can't unlock $dir/svc_acct.username: $!";
1594 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1598 sub radius_usergroup_selector {
1599 my $sel_groups = shift;
1600 my %sel_groups = map { $_=>1 } @$sel_groups;
1602 my $selectname = shift || 'radius_usergroup';
1605 my $sth = $dbh->prepare(
1606 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1607 ) or die $dbh->errstr;
1608 $sth->execute() or die $sth->errstr;
1609 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1613 function ${selectname}_doadd(object) {
1614 var myvalue = object.${selectname}_add.value;
1615 var optionName = new Option(myvalue,myvalue,false,true);
1616 var length = object.$selectname.length;
1617 object.$selectname.options[length] = optionName;
1618 object.${selectname}_add.value = "";
1621 <SELECT MULTIPLE NAME="$selectname">
1624 foreach my $group ( @all_groups ) {
1625 $html .= qq(<OPTION VALUE="$group");
1626 if ( $sel_groups{$group} ) {
1627 $html .= ' SELECTED';
1628 $sel_groups{$group} = 0;
1630 $html .= ">$group</OPTION>\n";
1632 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1633 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
1635 $html .= '</SELECT>';
1637 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1638 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1647 The $recref stuff in sub check should be cleaned up.
1649 The suspend, unsuspend and cancel methods update the database, but not the
1650 current object. This is probably a bug as it's unexpected and
1653 radius_usergroup_selector? putting web ui components in here? they should
1654 probably live somewhere else...
1656 insertion of RADIUS group stuff in insert could be done with child_objects now
1657 (would probably clean up export of them too)
1661 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1662 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1663 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1664 L<freeside-queued>), L<FS::svc_acct_pop>,
1665 schema.html from the base documentation.