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
10 $password_noampersand $password_noexclamation
11 $welcome_template $welcome_from $welcome_subject $welcome_mimetype
13 $radius_password $radius_ip
19 use FS::UID qw( datasrc );
21 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
26 use FS::cust_main_invoice;
30 use FS::radius_usergroup;
33 use FS::Msgcat qw(gettext);
37 @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 $password_noampersand = $conf->exists('password-noexclamation');
60 $password_noexclamation = $conf->exists('password-noexclamation');
61 $dirhash = $conf->config('dirhash') || 0;
62 if ( $conf->exists('welcome_email') ) {
63 $welcome_template = new Text::Template (
65 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
66 ) or warn "can't create welcome email template: $Text::Template::ERROR";
67 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
68 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
69 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
71 $welcome_template = '';
73 $welcome_subject = '';
74 $welcome_mimetype = '';
76 $smtpmachine = $conf->config('smtpmachine');
77 $radius_password = $conf->config('radius-password') || 'Password';
78 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
81 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
82 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
86 my ( $hashref, $cache ) = @_;
87 if ( $hashref->{'svc_acct_svcnum'} ) {
88 $self->{'_domsvc'} = FS::svc_domain->new( {
89 'svcnum' => $hashref->{'domsvc'},
90 'domain' => $hashref->{'svc_acct_domain'},
91 'catchall' => $hashref->{'svc_acct_catchall'},
98 FS::svc_acct - Object methods for svc_acct records
104 $record = new FS::svc_acct \%hash;
105 $record = new FS::svc_acct { 'column' => 'value' };
107 $error = $record->insert;
109 $error = $new_record->replace($old_record);
111 $error = $record->delete;
113 $error = $record->check;
115 $error = $record->suspend;
117 $error = $record->unsuspend;
119 $error = $record->cancel;
121 %hash = $record->radius;
123 %hash = $record->radius_reply;
125 %hash = $record->radius_check;
127 $domain = $record->domain;
129 $svc_domain = $record->svc_domain;
131 $email = $record->email;
133 $seconds_since = $record->seconds_since($timestamp);
137 An FS::svc_acct object represents an account. FS::svc_acct inherits from
138 FS::svc_Common. The following fields are currently supported:
142 =item svcnum - primary key (assigned automatcially for new accounts)
146 =item _password - generated if blank
148 =item sec_phrase - security phrase
150 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
158 =item dir - set automatically if blank (and uid is not)
162 =item quota - (unimplementd)
164 =item slipip - IP address
168 =item domsvc - svcnum from svc_domain
170 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
180 Creates a new account. To add the account to the database, see L<"insert">.
184 sub table { 'svc_acct'; }
186 =item insert [ , OPTION => VALUE ... ]
188 Adds this account to the database. If there is an error, returns the error,
189 otherwise returns false.
191 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
192 defined. An FS::cust_svc record will be created and inserted.
194 The additional field I<usergroup> can optionally be defined; if so it should
195 contain an arrayref of group names. See L<FS::radius_usergroup>.
197 The additional field I<child_objects> can optionally be defined; if so it
198 should contain an arrayref of FS::tablename objects. They will have their
199 svcnum fields set and will be inserted after this record, but before any
200 exports are run. Each element of the array can also optionally be a
201 two-element array reference containing the child object and the name of an
202 alternate field to be filled in with the newly-inserted svcnum, for example
203 C<[ $svc_forward, 'srcsvc' ]>
205 Currently available options are: I<depend_jobnum>
207 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
208 jobnums), all provisioning jobs will have a dependancy on the supplied
209 jobnum(s) (they will not run until the specific job(s) complete(s)).
211 (TODOC: L<FS::queue> and L<freeside-queued>)
213 (TODOC: new exports!)
222 local $SIG{HUP} = 'IGNORE';
223 local $SIG{INT} = 'IGNORE';
224 local $SIG{QUIT} = 'IGNORE';
225 local $SIG{TERM} = 'IGNORE';
226 local $SIG{TSTP} = 'IGNORE';
227 local $SIG{PIPE} = 'IGNORE';
229 my $oldAutoCommit = $FS::UID::AutoCommit;
230 local $FS::UID::AutoCommit = 0;
233 $error = $self->check;
234 return $error if $error;
236 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
237 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
238 unless ( $cust_svc ) {
239 $dbh->rollback if $oldAutoCommit;
240 return "no cust_svc record found for svcnum ". $self->svcnum;
242 $self->pkgnum($cust_svc->pkgnum);
243 $self->svcpart($cust_svc->svcpart);
246 $error = $self->_check_duplicate;
248 $dbh->rollback if $oldAutoCommit;
253 $error = $self->SUPER::insert(
254 'jobnums' => \@jobnums,
255 'child_objects' => $self->child_objects,
259 $dbh->rollback if $oldAutoCommit;
263 if ( $self->usergroup ) {
264 foreach my $groupname ( @{$self->usergroup} ) {
265 my $radius_usergroup = new FS::radius_usergroup ( {
266 svcnum => $self->svcnum,
267 groupname => $groupname,
269 my $error = $radius_usergroup->insert;
271 $dbh->rollback if $oldAutoCommit;
277 unless ( $skip_fuzzyfiles ) {
278 $error = $self->queue_fuzzyfiles_update;
280 $dbh->rollback if $oldAutoCommit;
281 return "updating fuzzy search cache: $error";
285 my $cust_pkg = $self->cust_svc->cust_pkg;
288 my $cust_main = $cust_pkg->cust_main;
290 if ( $conf->exists('emailinvoiceauto') ) {
291 my @invoicing_list = $cust_main->invoicing_list;
292 push @invoicing_list, $self->email;
293 $cust_main->invoicing_list(\@invoicing_list);
298 if ( $welcome_template && $cust_pkg ) {
299 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
301 my $wqueue = new FS::queue {
302 'svcnum' => $self->svcnum,
303 'job' => 'FS::svc_acct::send_email'
305 my $error = $wqueue->insert(
307 'from' => $welcome_from,
308 'subject' => $welcome_subject,
309 'mimetype' => $welcome_mimetype,
310 'body' => $welcome_template->fill_in( HASH => {
311 'custnum' => $self->custnum,
312 'username' => $self->username,
313 'password' => $self->_password,
314 'first' => $cust_main->first,
315 'last' => $cust_main->getfield('last'),
316 'pkg' => $cust_pkg->part_pkg->pkg,
320 $dbh->rollback if $oldAutoCommit;
321 return "error queuing welcome email: $error";
324 if ( $options{'depend_jobnum'} ) {
325 warn "$me depend_jobnum found; adding to welcome email dependancies"
327 if ( ref($options{'depend_jobnum'}) ) {
328 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
329 "to welcome email dependancies"
331 push @jobnums, @{ $options{'depend_jobnum'} };
333 warn "$me adding job $options{'depend_jobnum'} ".
334 "to welcome email dependancies"
336 push @jobnums, $options{'depend_jobnum'};
340 foreach my $jobnum ( @jobnums ) {
341 my $error = $wqueue->depend_insert($jobnum);
343 $dbh->rollback if $oldAutoCommit;
344 return "error queuing welcome email job dependancy: $error";
354 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
360 Deletes this account from the database. If there is an error, returns the
361 error, otherwise returns false.
363 The corresponding FS::cust_svc record will be deleted as well.
365 (TODOC: new exports!)
372 return "can't delete system account" if $self->_check_system;
374 return "Can't delete an account which is a (svc_forward) source!"
375 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
377 return "Can't delete an account which is a (svc_forward) destination!"
378 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
380 return "Can't delete an account with (svc_www) web service!"
381 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
383 # what about records in session ? (they should refer to history table)
385 local $SIG{HUP} = 'IGNORE';
386 local $SIG{INT} = 'IGNORE';
387 local $SIG{QUIT} = 'IGNORE';
388 local $SIG{TERM} = 'IGNORE';
389 local $SIG{TSTP} = 'IGNORE';
390 local $SIG{PIPE} = 'IGNORE';
392 my $oldAutoCommit = $FS::UID::AutoCommit;
393 local $FS::UID::AutoCommit = 0;
396 foreach my $cust_main_invoice (
397 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
399 unless ( defined($cust_main_invoice) ) {
400 warn "WARNING: something's wrong with qsearch";
403 my %hash = $cust_main_invoice->hash;
404 $hash{'dest'} = $self->email;
405 my $new = new FS::cust_main_invoice \%hash;
406 my $error = $new->replace($cust_main_invoice);
408 $dbh->rollback if $oldAutoCommit;
413 foreach my $svc_domain (
414 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
416 my %hash = new FS::svc_domain->hash;
417 $hash{'catchall'} = '';
418 my $new = new FS::svc_domain \%hash;
419 my $error = $new->replace($svc_domain);
421 $dbh->rollback if $oldAutoCommit;
426 foreach my $radius_usergroup (
427 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
429 my $error = $radius_usergroup->delete;
431 $dbh->rollback if $oldAutoCommit;
436 my $error = $self->SUPER::delete;
438 $dbh->rollback if $oldAutoCommit;
442 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
446 =item replace OLD_RECORD
448 Replaces OLD_RECORD with this one in the database. If there is an error,
449 returns the error, otherwise returns false.
451 The additional field I<usergroup> can optionally be defined; if so it should
452 contain an arrayref of group names. See L<FS::radius_usergroup>.
458 my ( $new, $old ) = ( shift, shift );
460 warn "$me replacing $old with $new\n" if $DEBUG;
462 return "can't modify system account" if $old->_check_system;
464 return "Username in use"
465 if $old->username ne $new->username &&
466 qsearchs( 'svc_acct', { 'username' => $new->username,
467 'domsvc' => $new->domsvc,
470 #no warnings 'numeric'; #alas, a 5.006-ism
472 return "Can't change uid!" if $old->uid != $new->uid;
475 #change homdir when we change username
476 $new->setfield('dir', '') if $old->username ne $new->username;
478 local $SIG{HUP} = 'IGNORE';
479 local $SIG{INT} = 'IGNORE';
480 local $SIG{QUIT} = 'IGNORE';
481 local $SIG{TERM} = 'IGNORE';
482 local $SIG{TSTP} = 'IGNORE';
483 local $SIG{PIPE} = 'IGNORE';
485 my $oldAutoCommit = $FS::UID::AutoCommit;
486 local $FS::UID::AutoCommit = 0;
489 # redundant, but so $new->usergroup gets set
490 $error = $new->check;
491 return $error if $error;
493 $old->usergroup( [ $old->radius_groups ] );
494 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
495 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
496 if ( $new->usergroup ) {
497 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
498 my @newgroups = @{$new->usergroup};
499 foreach my $oldgroup ( @{$old->usergroup} ) {
500 if ( grep { $oldgroup eq $_ } @newgroups ) {
501 @newgroups = grep { $oldgroup ne $_ } @newgroups;
504 my $radius_usergroup = qsearchs('radius_usergroup', {
505 svcnum => $old->svcnum,
506 groupname => $oldgroup,
508 my $error = $radius_usergroup->delete;
510 $dbh->rollback if $oldAutoCommit;
511 return "error deleting radius_usergroup $oldgroup: $error";
515 foreach my $newgroup ( @newgroups ) {
516 my $radius_usergroup = new FS::radius_usergroup ( {
517 svcnum => $new->svcnum,
518 groupname => $newgroup,
520 my $error = $radius_usergroup->insert;
522 $dbh->rollback if $oldAutoCommit;
523 return "error adding radius_usergroup $newgroup: $error";
529 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
530 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
531 $error = $new->_check_duplicate;
533 $dbh->rollback if $oldAutoCommit;
538 $error = $new->SUPER::replace($old);
540 $dbh->rollback if $oldAutoCommit;
541 return $error if $error;
544 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
545 $error = $new->queue_fuzzyfiles_update;
547 $dbh->rollback if $oldAutoCommit;
548 return "updating fuzzy search cache: $error";
552 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
556 =item queue_fuzzyfiles_update
558 Used by insert & replace to update the fuzzy search cache
562 sub queue_fuzzyfiles_update {
565 local $SIG{HUP} = 'IGNORE';
566 local $SIG{INT} = 'IGNORE';
567 local $SIG{QUIT} = 'IGNORE';
568 local $SIG{TERM} = 'IGNORE';
569 local $SIG{TSTP} = 'IGNORE';
570 local $SIG{PIPE} = 'IGNORE';
572 my $oldAutoCommit = $FS::UID::AutoCommit;
573 local $FS::UID::AutoCommit = 0;
576 my $queue = new FS::queue {
577 'svcnum' => $self->svcnum,
578 'job' => 'FS::svc_acct::append_fuzzyfiles'
580 my $error = $queue->insert($self->username);
582 $dbh->rollback if $oldAutoCommit;
583 return "queueing job (transaction rolled back): $error";
586 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
594 Suspends this account by calling export-specific suspend hooks. If there is
595 an error, returns the error, otherwise returns false.
597 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
603 return "can't suspend system account" if $self->_check_system;
604 $self->SUPER::suspend;
609 Unsuspends this account by by calling export-specific suspend hooks. If there
610 is an error, returns the error, otherwise returns false.
612 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
618 my %hash = $self->hash;
619 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
620 $hash{_password} = $1;
621 my $new = new FS::svc_acct ( \%hash );
622 my $error = $new->replace($self);
623 return $error if $error;
626 $self->SUPER::unsuspend;
631 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
633 If the B<auto_unset_catchall> configuration option is set, this method will
634 automatically remove any references to the canceled service in the catchall
635 field of svc_domain. This allows packages that contain both a svc_domain and
636 its catchall svc_acct to be canceled in one step.
641 # Only one thing to do at this level
643 foreach my $svc_domain (
644 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
645 if($conf->exists('auto_unset_catchall')) {
646 my %hash = $svc_domain->hash;
647 $hash{catchall} = '';
648 my $new = new FS::svc_domain ( \%hash );
649 my $error = $new->replace($svc_domain);
650 return $error if $error;
652 return "cannot unprovision svc_acct #".$self->svcnum.
653 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
657 $self->SUPER::cancel;
663 Checks all fields to make sure this is a valid service. If there is an error,
664 returns the error, otherwise returns false. Called by the insert and replace
667 Sets any fixed values; see L<FS::part_svc>.
674 my($recref) = $self->hashref;
676 my $x = $self->setfixed;
677 return $x unless ref($x);
680 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
682 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
685 my $error = $self->ut_numbern('svcnum')
686 #|| $self->ut_number('domsvc')
687 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
688 || $self->ut_textn('sec_phrase')
690 return $error if $error;
692 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
693 if ( $username_uppercase ) {
694 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
695 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
696 $recref->{username} = $1;
698 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
699 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
700 $recref->{username} = $1;
703 if ( $username_letterfirst ) {
704 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
705 } elsif ( $username_letter ) {
706 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
708 if ( $username_noperiod ) {
709 $recref->{username} =~ /\./ and return gettext('illegal_username');
711 if ( $username_nounderscore ) {
712 $recref->{username} =~ /_/ and return gettext('illegal_username');
714 if ( $username_nodash ) {
715 $recref->{username} =~ /\-/ and return gettext('illegal_username');
717 unless ( $username_ampersand ) {
718 $recref->{username} =~ /\&/ and return gettext('illegal_username');
720 if ( $password_noampersand ) {
721 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
723 if ( $password_noexclamation ) {
724 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
727 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
728 $recref->{popnum} = $1;
729 return "Unknown popnum" unless
730 ! $recref->{popnum} ||
731 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
733 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
735 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
736 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
738 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
739 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
740 #not all systems use gid=uid
741 #you can set a fixed gid in part_svc
743 return "Only root can have uid 0"
744 if $recref->{uid} == 0
745 && $recref->{username} !~ /^(root|toor|smtp)$/;
747 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
748 or return "Illegal directory: ". $recref->{dir};
750 return "Illegal directory"
751 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
752 return "Illegal directory"
753 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
754 unless ( $recref->{dir} ) {
755 $recref->{dir} = $dir_prefix . '/';
756 if ( $dirhash > 0 ) {
757 for my $h ( 1 .. $dirhash ) {
758 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
760 } elsif ( $dirhash < 0 ) {
761 for my $h ( reverse $dirhash .. -1 ) {
762 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
765 $recref->{dir} .= $recref->{username};
769 unless ( $recref->{username} eq 'sync' ) {
770 if ( grep $_ eq $recref->{shell}, @shells ) {
771 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
773 return "Illegal shell \`". $self->shell. "\'; ".
774 $conf->dir. "/shells contains: @shells";
777 $recref->{shell} = '/bin/sync';
781 $recref->{gid} ne '' ?
782 return "Can't have gid without uid" : ( $recref->{gid}='' );
783 $recref->{dir} ne '' ?
784 return "Can't have directory without uid" : ( $recref->{dir}='' );
785 $recref->{shell} ne '' ?
786 return "Can't have shell without uid" : ( $recref->{shell}='' );
789 # $error = $self->ut_textn('finger');
790 # return $error if $error;
791 if ( $self->getfield('finger') eq '' ) {
792 my $cust_pkg = $self->svcnum
793 ? $self->cust_svc->cust_pkg
794 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
796 my $cust_main = $cust_pkg->cust_main;
797 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
800 $self->getfield('finger') =~
801 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
802 or return "Illegal finger: ". $self->getfield('finger');
803 $self->setfield('finger', $1);
805 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
806 $recref->{quota} = $1;
808 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
809 if ( $recref->{slipip} eq '' ) {
810 $recref->{slipip} = '';
811 } elsif ( $recref->{slipip} eq '0e0' ) {
812 $recref->{slipip} = '0e0';
814 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
815 or return "Illegal slipip: ". $self->slipip;
816 $recref->{slipip} = $1;
821 #arbitrary RADIUS stuff; allow ut_textn for now
822 foreach ( grep /^radius_/, fields('svc_acct') ) {
826 #generate a password if it is blank
827 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
828 unless ( $recref->{_password} );
830 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
831 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
832 $recref->{_password} = $1.$3;
833 #uncomment this to encrypt password immediately upon entry, or run
834 #bin/crypt_pw in cron to give new users a window during which their
835 #password is available to techs, for faxing, etc. (also be aware of
837 #$recref->{password} = $1.
838 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
840 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
841 $recref->{_password} = $1.$3;
842 } elsif ( $recref->{_password} eq '*' ) {
843 $recref->{_password} = '*';
844 } elsif ( $recref->{_password} eq '!' ) {
845 $recref->{_password} = '!';
846 } elsif ( $recref->{_password} eq '!!' ) {
847 $recref->{_password} = '!!';
849 #return "Illegal password";
850 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
851 FS::Msgcat::_gettext('illegal_password_characters').
852 ": ". $recref->{_password};
860 Internal function to check the username against the list of system usernames
861 from the I<system_usernames> configuration value. Returns true if the username
862 is listed on the system username list.
868 scalar( grep { $self->username eq $_ || $self->email eq $_ }
869 $conf->config('system_usernames')
873 =item _check_duplicate
875 Internal function to check for duplicates usernames, username@domain pairs and
878 If the I<global_unique-username> configuration value is set to B<username> or
879 B<username@domain>, enforces global username or username@domain uniqueness.
881 In all cases, check for duplicate uids and usernames or username@domain pairs
882 per export and with identical I<svcpart> values.
886 sub _check_duplicate {
889 #this is Pg-specific. what to do for mysql etc?
890 # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
891 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
892 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
894 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
896 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
897 unless ( $part_svc ) {
898 return 'unknown svcpart '. $self->svcpart;
901 my $global_unique = $conf->config('global_unique-username') || 'none';
903 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
904 qsearch( 'svc_acct', { 'username' => $self->username } );
905 return gettext('username_in_use')
906 if $global_unique eq 'username' && @dup_user;
908 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
909 qsearch( 'svc_acct', { 'username' => $self->username,
910 'domsvc' => $self->domsvc } );
911 return gettext('username_in_use')
912 if $global_unique eq 'username@domain' && @dup_userdomain;
915 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
916 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
917 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
918 qsearch( 'svc_acct', { 'uid' => $self->uid } );
923 if ( @dup_user || @dup_userdomain || @dup_uid ) {
924 my $exports = FS::part_export::export_info('svc_acct');
925 my %conflict_user_svcpart;
926 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
928 foreach my $part_export ( $part_svc->part_export ) {
930 #this will catch to the same exact export
931 my @svcparts = map { $_->svcpart } $part_export->export_svc;
933 #this will catch to exports w/same exporthost+type ???
934 #my @other_part_export = qsearch('part_export', {
935 # 'machine' => $part_export->machine,
936 # 'exporttype' => $part_export->exporttype,
938 #foreach my $other_part_export ( @other_part_export ) {
939 # push @svcparts, map { $_->svcpart }
940 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
943 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
944 #silly kludge to avoid uninitialized value errors
945 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
946 ? $exports->{$part_export->exporttype}{'nodomain'}
948 if ( $nodomain =~ /^Y/i ) {
949 $conflict_user_svcpart{$_} = $part_export->exportnum
952 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
957 foreach my $dup_user ( @dup_user ) {
958 my $dup_svcpart = $dup_user->cust_svc->svcpart;
959 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
960 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
961 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
965 foreach my $dup_userdomain ( @dup_userdomain ) {
966 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
967 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
968 return "duplicate username\@domain: conflicts with svcnum ".
969 $dup_userdomain->svcnum. " via exportnum ".
970 $conflict_userdomain_svcpart{$dup_svcpart};
974 foreach my $dup_uid ( @dup_uid ) {
975 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
976 if ( exists($conflict_user_svcpart{$dup_svcpart})
977 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
978 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
979 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
980 || $conflict_userdomain_svcpart{$dup_svcpart};
992 Depriciated, use radius_reply instead.
997 carp "FS::svc_acct::radius depriciated, use radius_reply";
1003 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1004 reply attributes of this record.
1006 Note that this is now the preferred method for reading RADIUS attributes -
1007 accessing the columns directly is discouraged, as the column names are
1008 expected to change in the future.
1017 my($column, $attrib) = ($1, $2);
1018 #$attrib =~ s/_/\-/g;
1019 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1020 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1021 if ( $self->slipip && $self->slipip ne '0e0' ) {
1022 $reply{$radius_ip} = $self->slipip;
1024 if ( $self->seconds !~ /^$/ ) {
1025 $reply{'Session-Timeout'} = $self->seconds;
1032 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1033 check attributes of this record.
1035 Note that this is now the preferred method for reading RADIUS attributes -
1036 accessing the columns directly is discouraged, as the column names are
1037 expected to change in the future.
1043 my $password = $self->_password;
1044 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
1045 ( $pw_attrib => $password,
1048 my($column, $attrib) = ($1, $2);
1049 #$attrib =~ s/_/\-/g;
1050 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1051 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
1057 Returns the domain associated with this account.
1063 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1064 my $svc_domain = $self->svc_domain(@_)
1065 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1066 $svc_domain->domain;
1071 Returns the FS::svc_domain record for this account's domain (see
1079 ? $self->{'_domsvc'}
1080 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1085 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1091 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
1096 Returns an email address associated with the account.
1102 $self->username. '@'. $self->domain(@_);
1107 Returns an array of FS::acct_snarf records associated with the account.
1108 If the acct_snarf table does not exist or there are no associated records,
1109 an empty list is returned
1115 return () unless dbdef->table('acct_snarf');
1116 eval "use FS::acct_snarf;";
1118 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1121 =item seconds_since TIMESTAMP
1123 Returns the number of seconds this account has been online since TIMESTAMP,
1124 according to the session monitor (see L<FS::Session>).
1126 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1127 L<Time::Local> and L<Date::Parse> for conversion functions.
1131 #note: POD here, implementation in FS::cust_svc
1134 $self->cust_svc->seconds_since(@_);
1137 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1139 Returns the numbers of seconds this account has been online between
1140 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1141 external SQL radacct table, specified via sqlradius export. Sessions which
1142 started in the specified range but are still open are counted from session
1143 start to the end of the range (unless they are over 1 day old, in which case
1144 they are presumed missing their stop record and not counted). Also, sessions
1145 which end in the range but started earlier are counted from the start of the
1146 range to session end. Finally, sessions which start before the range but end
1147 after are counted for the entire range.
1149 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1150 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1155 #note: POD here, implementation in FS::cust_svc
1156 sub seconds_since_sqlradacct {
1158 $self->cust_svc->seconds_since_sqlradacct(@_);
1161 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1163 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1164 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1165 TIMESTAMP_END (exclusive).
1167 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1168 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1173 #note: POD here, implementation in FS::cust_svc
1174 sub attribute_since_sqlradacct {
1176 $self->cust_svc->attribute_since_sqlradacct(@_);
1179 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1181 Returns an array of hash references of this customers login history for the
1182 given time range. (document this better)
1186 sub get_session_history {
1188 $self->cust_svc->get_session_history(@_);
1193 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1199 if ( $self->usergroup ) {
1200 #when provisioning records, export callback runs in svc_Common.pm before
1201 #radius_usergroup records can be inserted...
1202 @{$self->usergroup};
1204 map { $_->groupname }
1205 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1209 =item clone_suspended
1211 Constructor used by FS::part_export::_export_suspend fallback. Document
1216 sub clone_suspended {
1218 my %hash = $self->hash;
1219 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1220 new FS::svc_acct \%hash;
1223 =item clone_kludge_unsuspend
1225 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1230 sub clone_kludge_unsuspend {
1232 my %hash = $self->hash;
1233 $hash{_password} = '';
1234 new FS::svc_acct \%hash;
1237 =item check_password
1239 Checks the supplied password against the (possibly encrypted) password in the
1240 database. Returns true for a sucessful authentication, false for no match.
1242 Currently supported encryptions are: classic DES crypt() and MD5
1246 sub check_password {
1247 my($self, $check_password) = @_;
1249 #remove old-style SUSPENDED kludge, they should be allowed to login to
1250 #self-service and pay up
1251 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1253 #eventually should check a "password-encoding" field
1254 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1256 } elsif ( length($password) < 13 ) { #plaintext
1257 $check_password eq $password;
1258 } elsif ( length($password) == 13 ) { #traditional DES crypt
1259 crypt($check_password, $password) eq $password;
1260 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1261 unix_md5_crypt($check_password, $password) eq $password;
1262 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1263 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1264 $self->svcnum. "\n";
1267 warn "Can't check password: Unrecognized encryption for svcnum ".
1268 $self->svcnum. "\n";
1274 =item crypt_password
1276 Returns an encrypted password, either by passing through an encrypted password
1277 in the database or by encrypting a plaintext password from the database.
1281 sub crypt_password {
1283 #false laziness w/shellcommands.pm
1284 #eventually should check a "password-encoding" field
1285 if ( length($self->_password) == 13
1286 || $self->_password =~ /^\$(1|2a?)\$/ ) {
1291 $saltset[int(rand(64))].$saltset[int(rand(64))]
1296 =item virtual_maildir
1298 Returns $domain/maildirs/$username/
1302 sub virtual_maildir {
1304 $self->domain. '/maildirs/'. $self->username. '/';
1315 This is the FS::svc_acct job-queue-able version. It still uses
1316 FS::Misc::send_email under-the-hood.
1323 eval "use FS::Misc qw(send_email)";
1326 $opt{mimetype} ||= 'text/plain';
1327 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1329 my $error = send_email(
1330 'from' => $opt{from},
1332 'subject' => $opt{subject},
1333 'content-type' => $opt{mimetype},
1334 'body' => [ map "$_\n", split("\n", $opt{body}) ],
1336 die $error if $error;
1339 =item check_and_rebuild_fuzzyfiles
1343 sub check_and_rebuild_fuzzyfiles {
1344 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1345 -e "$dir/svc_acct.username"
1346 or &rebuild_fuzzyfiles;
1349 =item rebuild_fuzzyfiles
1353 sub rebuild_fuzzyfiles {
1355 use Fcntl qw(:flock);
1357 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1361 open(USERNAMELOCK,">>$dir/svc_acct.username")
1362 or die "can't open $dir/svc_acct.username: $!";
1363 flock(USERNAMELOCK,LOCK_EX)
1364 or die "can't lock $dir/svc_acct.username: $!";
1366 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1368 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1369 or die "can't open $dir/svc_acct.username.tmp: $!";
1370 print USERNAMECACHE join("\n", @all_username), "\n";
1371 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1373 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1383 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1384 open(USERNAMECACHE,"<$dir/svc_acct.username")
1385 or die "can't open $dir/svc_acct.username: $!";
1386 my @array = map { chomp; $_; } <USERNAMECACHE>;
1387 close USERNAMECACHE;
1391 =item append_fuzzyfiles USERNAME
1395 sub append_fuzzyfiles {
1396 my $username = shift;
1398 &check_and_rebuild_fuzzyfiles;
1400 use Fcntl qw(:flock);
1402 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1404 open(USERNAME,">>$dir/svc_acct.username")
1405 or die "can't open $dir/svc_acct.username: $!";
1406 flock(USERNAME,LOCK_EX)
1407 or die "can't lock $dir/svc_acct.username: $!";
1409 print USERNAME "$username\n";
1411 flock(USERNAME,LOCK_UN)
1412 or die "can't unlock $dir/svc_acct.username: $!";
1420 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1424 sub radius_usergroup_selector {
1425 my $sel_groups = shift;
1426 my %sel_groups = map { $_=>1 } @$sel_groups;
1428 my $selectname = shift || 'radius_usergroup';
1431 my $sth = $dbh->prepare(
1432 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1433 ) or die $dbh->errstr;
1434 $sth->execute() or die $sth->errstr;
1435 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1439 function ${selectname}_doadd(object) {
1440 var myvalue = object.${selectname}_add.value;
1441 var optionName = new Option(myvalue,myvalue,false,true);
1442 var length = object.$selectname.length;
1443 object.$selectname.options[length] = optionName;
1444 object.${selectname}_add.value = "";
1447 <SELECT MULTIPLE NAME="$selectname">
1450 foreach my $group ( @all_groups ) {
1451 $html .= qq(<OPTION VALUE="$group");
1452 if ( $sel_groups{$group} ) {
1453 $html .= ' SELECTED';
1454 $sel_groups{$group} = 0;
1456 $html .= ">$group</OPTION>\n";
1458 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1459 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
1461 $html .= '</SELECT>';
1463 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1464 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1473 The $recref stuff in sub check should be cleaned up.
1475 The suspend, unsuspend and cancel methods update the database, but not the
1476 current object. This is probably a bug as it's unexpected and
1479 radius_usergroup_selector? putting web ui components in here? they should
1480 probably live somewhere else...
1482 insertion of RADIUS group stuff in insert could be done with child_objects now
1483 (would probably clean up export of them too)
1487 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1488 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1489 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1490 L<freeside-queued>), L<FS::svc_acct_pop>,
1491 schema.html from the base documentation.