4 use vars qw( @ISA $DEBUG $me $conf
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
12 $welcome_template $welcome_from $welcome_subject $welcome_mimetype
14 $radius_password $radius_ip
20 use FS::UID qw( datasrc );
22 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
29 use FS::cust_main_invoice;
33 use FS::radius_usergroup;
36 use FS::Msgcat qw(gettext);
40 @ISA = qw( FS::svc_Common );
44 $me = '[FS::svc_acct]';
46 #ask FS::UID to run this stuff for us later
47 $FS::UID::callback{'FS::svc_acct'} = sub {
49 $dir_prefix = $conf->config('home');
50 @shells = $conf->config('shells');
51 $usernamemin = $conf->config('usernamemin') || 2;
52 $usernamemax = $conf->config('usernamemax');
53 $passwordmin = $conf->config('passwordmin') || 6;
54 $passwordmax = $conf->config('passwordmax') || 8;
55 $username_letter = $conf->exists('username-letter');
56 $username_letterfirst = $conf->exists('username-letterfirst');
57 $username_noperiod = $conf->exists('username-noperiod');
58 $username_nounderscore = $conf->exists('username-nounderscore');
59 $username_nodash = $conf->exists('username-nodash');
60 $username_uppercase = $conf->exists('username-uppercase');
61 $username_ampersand = $conf->exists('username-ampersand');
62 $password_noampersand = $conf->exists('password-noexclamation');
63 $password_noexclamation = $conf->exists('password-noexclamation');
64 $mydomain = $conf->config('domain');
65 $dirhash = $conf->config('dirhash') || 0;
66 if ( $conf->exists('welcome_email') ) {
67 $welcome_template = new Text::Template (
69 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
70 ) or warn "can't create welcome email template: $Text::Template::ERROR";
71 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
72 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
73 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
75 $welcome_template = '';
77 $welcome_subject = '';
78 $welcome_mimetype = '';
80 $smtpmachine = $conf->config('smtpmachine');
81 $radius_password = $conf->config('radius-password') || 'Password';
82 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
85 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
86 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
90 my ( $hashref, $cache ) = @_;
91 if ( $hashref->{'svc_acct_svcnum'} ) {
92 $self->{'_domsvc'} = FS::svc_domain->new( {
93 'svcnum' => $hashref->{'domsvc'},
94 'domain' => $hashref->{'svc_acct_domain'},
95 'catchall' => $hashref->{'svc_acct_catchall'},
102 FS::svc_acct - Object methods for svc_acct records
108 $record = new FS::svc_acct \%hash;
109 $record = new FS::svc_acct { 'column' => 'value' };
111 $error = $record->insert;
113 $error = $new_record->replace($old_record);
115 $error = $record->delete;
117 $error = $record->check;
119 $error = $record->suspend;
121 $error = $record->unsuspend;
123 $error = $record->cancel;
125 %hash = $record->radius;
127 %hash = $record->radius_reply;
129 %hash = $record->radius_check;
131 $domain = $record->domain;
133 $svc_domain = $record->svc_domain;
135 $email = $record->email;
137 $seconds_since = $record->seconds_since($timestamp);
141 An FS::svc_acct object represents an account. FS::svc_acct inherits from
142 FS::svc_Common. The following fields are currently supported:
146 =item svcnum - primary key (assigned automatcially for new accounts)
150 =item _password - generated if blank
152 =item sec_phrase - security phrase
154 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
162 =item dir - set automatically if blank (and uid is not)
166 =item quota - (unimplementd)
168 =item slipip - IP address
172 =item domsvc - svcnum from svc_domain
174 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
184 Creates a new account. To add the account to the database, see L<"insert">.
188 sub table { 'svc_acct'; }
190 =item insert [ , OPTION => VALUE ... ]
192 Adds this account to the database. If there is an error, returns the error,
193 otherwise returns false.
195 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
196 defined. An FS::cust_svc record will be created and inserted.
198 The additional field I<usergroup> can optionally be defined; if so it should
199 contain an arrayref of group names. See L<FS::radius_usergroup>.
201 The additional field I<child_objects> can optionally be defined; if so it
202 should contain an arrayref of FS::tablename objects. They will have their
203 svcnum fields set and will be inserted after this record, but before any
206 Currently available options are: I<depend_jobnum>
208 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
209 jobnums), all provisioning jobs will have a dependancy on the supplied
210 jobnum(s) (they will not run until the specific job(s) complete(s)).
212 (TODOC: L<FS::queue> and L<freeside-queued>)
214 (TODOC: new exports!)
223 local $SIG{HUP} = 'IGNORE';
224 local $SIG{INT} = 'IGNORE';
225 local $SIG{QUIT} = 'IGNORE';
226 local $SIG{TERM} = 'IGNORE';
227 local $SIG{TSTP} = 'IGNORE';
228 local $SIG{PIPE} = 'IGNORE';
230 my $oldAutoCommit = $FS::UID::AutoCommit;
231 local $FS::UID::AutoCommit = 0;
234 $error = $self->check;
235 return $error if $error;
237 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
238 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
239 unless ( $cust_svc ) {
240 $dbh->rollback if $oldAutoCommit;
241 return "no cust_svc record found for svcnum ". $self->svcnum;
243 $self->pkgnum($cust_svc->pkgnum);
244 $self->svcpart($cust_svc->svcpart);
247 $error = $self->_check_duplicate;
249 $dbh->rollback if $oldAutoCommit;
254 $error = $self->SUPER::insert(
255 'jobnums' => \@jobnums,
256 'child_objects' => $self->child_objects,
260 $dbh->rollback if $oldAutoCommit;
264 if ( $self->usergroup ) {
265 foreach my $groupname ( @{$self->usergroup} ) {
266 my $radius_usergroup = new FS::radius_usergroup ( {
267 svcnum => $self->svcnum,
268 groupname => $groupname,
270 my $error = $radius_usergroup->insert;
272 $dbh->rollback if $oldAutoCommit;
278 #false laziness with sub replace (and cust_main)
279 my $queue = new FS::queue {
280 'svcnum' => $self->svcnum,
281 'job' => 'FS::svc_acct::append_fuzzyfiles'
283 $error = $queue->insert($self->username);
285 $dbh->rollback if $oldAutoCommit;
286 return "queueing job (transaction rolled back): $error";
289 my $cust_pkg = $self->cust_svc->cust_pkg;
292 my $cust_main = $cust_pkg->cust_main;
294 if ( $conf->exists('emailinvoiceauto') ) {
295 my @invoicing_list = $cust_main->invoicing_list;
296 push @invoicing_list, $self->email;
297 $cust_main->invoicing_list(\@invoicing_list);
302 if ( $welcome_template && $cust_pkg ) {
303 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
305 my $wqueue = new FS::queue {
306 'svcnum' => $self->svcnum,
307 'job' => 'FS::svc_acct::send_email'
309 my $error = $wqueue->insert(
311 'from' => $welcome_from,
312 'subject' => $welcome_subject,
313 'mimetype' => $welcome_mimetype,
314 'body' => $welcome_template->fill_in( HASH => {
315 'custnum' => $self->custnum,
316 'username' => $self->username,
317 'password' => $self->_password,
318 'first' => $cust_main->first,
319 'last' => $cust_main->getfield('last'),
320 'pkg' => $cust_pkg->part_pkg->pkg,
324 $dbh->rollback if $oldAutoCommit;
325 return "error queuing welcome email: $error";
328 if ( $options{'depend_jobnum'} ) {
329 warn "$me depend_jobnum found; adding to welcome email dependancies"
331 if ( ref($options{'depend_jobnum'}) ) {
332 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
333 "to welcome email dependancies"
335 push @jobnums, @{ $options{'depend_jobnum'} };
337 warn "$me adding job $options{'depend_jobnum'} ".
338 "to welcome email dependancies"
340 push @jobnums, $options{'depend_jobnum'};
344 foreach my $jobnum ( @jobnums ) {
345 my $error = $wqueue->depend_insert($jobnum);
347 $dbh->rollback if $oldAutoCommit;
348 return "error queuing welcome email job dependancy: $error";
358 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
364 Deletes this account from the database. If there is an error, returns the
365 error, otherwise returns false.
367 The corresponding FS::cust_svc record will be deleted as well.
369 (TODOC: new exports!)
376 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
377 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
378 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
381 return "can't delete system account" if $self->_check_system;
383 return "Can't delete an account which is a (svc_forward) source!"
384 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
386 return "Can't delete an account which is a (svc_forward) destination!"
387 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
389 return "Can't delete an account with (svc_www) web service!"
390 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
392 # what about records in session ? (they should refer to history table)
394 local $SIG{HUP} = 'IGNORE';
395 local $SIG{INT} = 'IGNORE';
396 local $SIG{QUIT} = 'IGNORE';
397 local $SIG{TERM} = 'IGNORE';
398 local $SIG{TSTP} = 'IGNORE';
399 local $SIG{PIPE} = 'IGNORE';
401 my $oldAutoCommit = $FS::UID::AutoCommit;
402 local $FS::UID::AutoCommit = 0;
405 foreach my $cust_main_invoice (
406 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
408 unless ( defined($cust_main_invoice) ) {
409 warn "WARNING: something's wrong with qsearch";
412 my %hash = $cust_main_invoice->hash;
413 $hash{'dest'} = $self->email;
414 my $new = new FS::cust_main_invoice \%hash;
415 my $error = $new->replace($cust_main_invoice);
417 $dbh->rollback if $oldAutoCommit;
422 foreach my $svc_domain (
423 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
425 my %hash = new FS::svc_domain->hash;
426 $hash{'catchall'} = '';
427 my $new = new FS::svc_domain \%hash;
428 my $error = $new->replace($svc_domain);
430 $dbh->rollback if $oldAutoCommit;
435 foreach my $radius_usergroup (
436 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
438 my $error = $radius_usergroup->delete;
440 $dbh->rollback if $oldAutoCommit;
445 my $error = $self->SUPER::delete;
447 $dbh->rollback if $oldAutoCommit;
451 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
455 =item replace OLD_RECORD
457 Replaces OLD_RECORD with this one in the database. If there is an error,
458 returns the error, otherwise returns false.
460 The additional field I<usergroup> can optionally be defined; if so it should
461 contain an arrayref of group names. See L<FS::radius_usergroup>.
467 my ( $new, $old ) = ( shift, shift );
469 warn "$me replacing $old with $new\n" if $DEBUG;
471 return "can't modify system account" if $old->_check_system;
473 return "Username in use"
474 if $old->username ne $new->username &&
475 qsearchs( 'svc_acct', { 'username' => $new->username,
476 'domsvc' => $new->domsvc,
479 #no warnings 'numeric'; #alas, a 5.006-ism
481 return "Can't change uid!" if $old->uid != $new->uid;
484 #change homdir when we change username
485 $new->setfield('dir', '') if $old->username ne $new->username;
487 local $SIG{HUP} = 'IGNORE';
488 local $SIG{INT} = 'IGNORE';
489 local $SIG{QUIT} = 'IGNORE';
490 local $SIG{TERM} = 'IGNORE';
491 local $SIG{TSTP} = 'IGNORE';
492 local $SIG{PIPE} = 'IGNORE';
494 my $oldAutoCommit = $FS::UID::AutoCommit;
495 local $FS::UID::AutoCommit = 0;
498 # redundant, but so $new->usergroup gets set
499 $error = $new->check;
500 return $error if $error;
502 $old->usergroup( [ $old->radius_groups ] );
503 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
504 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
505 if ( $new->usergroup ) {
506 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
507 my @newgroups = @{$new->usergroup};
508 foreach my $oldgroup ( @{$old->usergroup} ) {
509 if ( grep { $oldgroup eq $_ } @newgroups ) {
510 @newgroups = grep { $oldgroup ne $_ } @newgroups;
513 my $radius_usergroup = qsearchs('radius_usergroup', {
514 svcnum => $old->svcnum,
515 groupname => $oldgroup,
517 my $error = $radius_usergroup->delete;
519 $dbh->rollback if $oldAutoCommit;
520 return "error deleting radius_usergroup $oldgroup: $error";
524 foreach my $newgroup ( @newgroups ) {
525 my $radius_usergroup = new FS::radius_usergroup ( {
526 svcnum => $new->svcnum,
527 groupname => $newgroup,
529 my $error = $radius_usergroup->insert;
531 $dbh->rollback if $oldAutoCommit;
532 return "error adding radius_usergroup $newgroup: $error";
538 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
539 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
540 $error = $new->_check_duplicate;
542 $dbh->rollback if $oldAutoCommit;
547 $error = $new->SUPER::replace($old);
549 $dbh->rollback if $oldAutoCommit;
550 return $error if $error;
553 if ( $new->username ne $old->username ) {
554 #false laziness with sub insert (and cust_main)
555 my $queue = new FS::queue {
556 'svcnum' => $new->svcnum,
557 'job' => 'FS::svc_acct::append_fuzzyfiles'
559 $error = $queue->insert($new->username);
561 $dbh->rollback if $oldAutoCommit;
562 return "queueing job (transaction rolled back): $error";
566 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
572 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
573 error, returns the error, otherwise returns false.
575 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
577 Calls any export-specific suspend hooks.
583 return "can't suspend system account" if $self->_check_system;
584 $self->SUPER::suspend;
589 Unsuspends this account by removing *SUSPENDED* from the password. If there is
590 an error, returns the error, otherwise returns false.
592 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
594 Calls any export-specific unsuspend hooks.
600 my %hash = $self->hash;
601 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
602 $hash{_password} = $1;
603 my $new = new FS::svc_acct ( \%hash );
604 my $error = $new->replace($self);
605 return $error if $error;
608 $self->SUPER::unsuspend;
613 Just returns false (no error) for now.
615 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
619 Checks all fields to make sure this is a valid service. If there is an error,
620 returns the error, otherwise returns false. Called by the insert and replace
623 Sets any fixed values; see L<FS::part_svc>.
630 my($recref) = $self->hashref;
632 my $x = $self->setfixed;
633 return $x unless ref($x);
636 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
638 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
641 my $error = $self->ut_numbern('svcnum')
642 #|| $self->ut_number('domsvc')
643 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
644 || $self->ut_textn('sec_phrase')
646 return $error if $error;
648 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
649 if ( $username_uppercase ) {
650 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
651 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
652 $recref->{username} = $1;
654 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
655 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
656 $recref->{username} = $1;
659 if ( $username_letterfirst ) {
660 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
661 } elsif ( $username_letter ) {
662 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
664 if ( $username_noperiod ) {
665 $recref->{username} =~ /\./ and return gettext('illegal_username');
667 if ( $username_nounderscore ) {
668 $recref->{username} =~ /_/ and return gettext('illegal_username');
670 if ( $username_nodash ) {
671 $recref->{username} =~ /\-/ and return gettext('illegal_username');
673 unless ( $username_ampersand ) {
674 $recref->{username} =~ /\&/ and return gettext('illegal_username');
676 if ( $password_noampersand ) {
677 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
679 if ( $password_noexclamation ) {
680 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
683 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
684 $recref->{popnum} = $1;
685 return "Unknown popnum" unless
686 ! $recref->{popnum} ||
687 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
689 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
691 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
692 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
694 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
695 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
696 #not all systems use gid=uid
697 #you can set a fixed gid in part_svc
699 return "Only root can have uid 0"
700 if $recref->{uid} == 0
701 && $recref->{username} !~ /^(root|toor|smtp)$/;
703 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
704 or return "Illegal directory: ". $recref->{dir};
706 return "Illegal directory"
707 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
708 return "Illegal directory"
709 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
710 unless ( $recref->{dir} ) {
711 $recref->{dir} = $dir_prefix . '/';
712 if ( $dirhash > 0 ) {
713 for my $h ( 1 .. $dirhash ) {
714 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
716 } elsif ( $dirhash < 0 ) {
717 for my $h ( reverse $dirhash .. -1 ) {
718 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
721 $recref->{dir} .= $recref->{username};
725 unless ( $recref->{username} eq 'sync' ) {
726 if ( grep $_ eq $recref->{shell}, @shells ) {
727 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
729 return "Illegal shell \`". $self->shell. "\'; ".
730 $conf->dir. "/shells contains: @shells";
733 $recref->{shell} = '/bin/sync';
737 $recref->{gid} ne '' ?
738 return "Can't have gid without uid" : ( $recref->{gid}='' );
739 $recref->{dir} ne '' ?
740 return "Can't have directory without uid" : ( $recref->{dir}='' );
741 $recref->{shell} ne '' ?
742 return "Can't have shell without uid" : ( $recref->{shell}='' );
745 # $error = $self->ut_textn('finger');
746 # return $error if $error;
747 if ( $self->getfield('finger') eq '' ) {
748 my $cust_pkg = $self->svcnum
749 ? $self->cust_svc->cust_pkg
750 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
752 my $cust_main = $cust_pkg->cust_main;
753 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
756 $self->getfield('finger') =~
757 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
758 or return "Illegal finger: ". $self->getfield('finger');
759 $self->setfield('finger', $1);
761 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
762 $recref->{quota} = $1;
764 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
765 if ( $recref->{slipip} eq '' ) {
766 $recref->{slipip} = '';
767 } elsif ( $recref->{slipip} eq '0e0' ) {
768 $recref->{slipip} = '0e0';
770 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
771 or return "Illegal slipip". $self->slipip;
772 $recref->{slipip} = $1;
777 #arbitrary RADIUS stuff; allow ut_textn for now
778 foreach ( grep /^radius_/, fields('svc_acct') ) {
782 #generate a password if it is blank
783 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
784 unless ( $recref->{_password} );
786 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
787 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
788 $recref->{_password} = $1.$3;
789 #uncomment this to encrypt password immediately upon entry, or run
790 #bin/crypt_pw in cron to give new users a window during which their
791 #password is available to techs, for faxing, etc. (also be aware of
793 #$recref->{password} = $1.
794 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
796 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
797 $recref->{_password} = $1.$3;
798 } elsif ( $recref->{_password} eq '*' ) {
799 $recref->{_password} = '*';
800 } elsif ( $recref->{_password} eq '!' ) {
801 $recref->{_password} = '!';
802 } elsif ( $recref->{_password} eq '!!' ) {
803 $recref->{_password} = '!!';
805 #return "Illegal password";
806 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
807 FS::Msgcat::_gettext('illegal_password_characters').
808 ": ". $recref->{_password};
820 scalar( grep { $self->username eq $_ || $self->email eq $_ }
821 $conf->config('system_usernames')
825 =item _check_duplicate
827 Internal function to check for duplicates usernames, username@domain pairs and
830 If the I<global_unique-username> configuration value is set to B<username> or
831 B<username@domain>, enforces global username or username@domain uniqueness.
833 In all cases, check for duplicate uids and usernames or username@domain pairs
834 per export and with identical I<svcpart> values.
838 sub _check_duplicate {
841 #this is Pg-specific. what to do for mysql etc?
842 # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
843 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
844 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
846 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
848 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
849 unless ( $part_svc ) {
850 return 'unknown svcpart '. $self->svcpart;
853 my $global_unique = $conf->config('global_unique-username');
855 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
856 qsearch( 'svc_acct', { 'username' => $self->username } );
857 return gettext('username_in_use')
858 if $global_unique eq 'username' && @dup_user;
860 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
861 qsearch( 'svc_acct', { 'username' => $self->username,
862 'domsvc' => $self->domsvc } );
863 return gettext('username_in_use')
864 if $global_unique eq 'username@domain' && @dup_userdomain;
867 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
868 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
869 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
870 qsearch( 'svc_acct', { 'uid' => $self->uid } );
875 if ( @dup_user || @dup_userdomain || @dup_uid ) {
876 my $exports = FS::part_export::export_info('svc_acct');
877 my %conflict_user_svcpart;
878 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
880 foreach my $part_export ( $part_svc->part_export ) {
882 #this will catch to the same exact export
883 my @svcparts = map { $_->svcpart } $part_export->export_svc;
885 #this will catch to exports w/same exporthost+type ???
886 #my @other_part_export = qsearch('part_export', {
887 # 'machine' => $part_export->machine,
888 # 'exporttype' => $part_export->exporttype,
890 #foreach my $other_part_export ( @other_part_export ) {
891 # push @svcparts, map { $_->svcpart }
892 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
895 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
896 #silly kludge to avoid uninitialized value errors
897 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
898 ? $exports->{$part_export->exporttype}{'nodomain'}
900 if ( $nodomain =~ /^Y/i ) {
901 $conflict_user_svcpart{$_} = $part_export->exportnum
904 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
909 foreach my $dup_user ( @dup_user ) {
910 my $dup_svcpart = $dup_user->cust_svc->svcpart;
911 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
912 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
913 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
917 foreach my $dup_userdomain ( @dup_userdomain ) {
918 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
919 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
920 return "duplicate username\@domain: conflicts with svcnum ".
921 $dup_userdomain->svcnum. " via exportnum ".
922 $conflict_userdomain_svcpart{$dup_svcpart};
926 foreach my $dup_uid ( @dup_uid ) {
927 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
928 if ( exists($conflict_user_svcpart{$dup_svcpart})
929 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
930 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
931 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
932 || $conflict_userdomain_svcpart{$dup_svcpart};
944 Depriciated, use radius_reply instead.
949 carp "FS::svc_acct::radius depriciated, use radius_reply";
955 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
956 reply attributes of this record.
958 Note that this is now the preferred method for reading RADIUS attributes -
959 accessing the columns directly is discouraged, as the column names are
960 expected to change in the future.
962 Internal function to check the username against the list of system usernames
963 from the I<system_usernames> configuration value. Returns true if the username
964 is listed on the system username list.
973 my($column, $attrib) = ($1, $2);
974 #$attrib =~ s/_/\-/g;
975 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
976 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
977 if ( $self->slipip && $self->slipip ne '0e0' ) {
978 $reply{$radius_ip} = $self->slipip;
985 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
986 check attributes of this record.
988 Note that this is now the preferred method for reading RADIUS attributes -
989 accessing the columns directly is discouraged, as the column names are
990 expected to change in the future.
996 my $password = $self->_password;
997 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
998 ( $pw_attrib => $self->_password,
1001 my($column, $attrib) = ($1, $2);
1002 #$attrib =~ s/_/\-/g;
1003 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1004 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
1010 Returns the domain associated with this account.
1016 if ( $self->domsvc ) {
1017 #$self->svc_domain->domain;
1018 my $svc_domain = $self->svc_domain
1019 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1020 $svc_domain->domain;
1022 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
1028 Returns the FS::svc_domain record for this account's domain (see
1036 ? $self->{'_domsvc'}
1037 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1042 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1048 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
1053 Returns an email address associated with the account.
1059 $self->username. '@'. $self->domain;
1064 Returns an array of FS::acct_snarf records associated with the account.
1065 If the acct_snarf table does not exist or there are no associated records,
1066 an empty list is returned
1072 return () unless dbdef->table('acct_snarf');
1073 eval "use FS::acct_snarf;";
1075 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1078 =item seconds_since TIMESTAMP
1080 Returns the number of seconds this account has been online since TIMESTAMP,
1081 according to the session monitor (see L<FS::Session>).
1083 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1084 L<Time::Local> and L<Date::Parse> for conversion functions.
1088 #note: POD here, implementation in FS::cust_svc
1091 $self->cust_svc->seconds_since(@_);
1094 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1096 Returns the numbers of seconds this account has been online between
1097 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1098 external SQL radacct table, specified via sqlradius export. Sessions which
1099 started in the specified range but are still open are counted from session
1100 start to the end of the range (unless they are over 1 day old, in which case
1101 they are presumed missing their stop record and not counted). Also, sessions
1102 which end in the range but started earlier are counted from the start of the
1103 range to session end. Finally, sessions which start before the range but end
1104 after are counted for the entire range.
1106 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1107 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1112 #note: POD here, implementation in FS::cust_svc
1113 sub seconds_since_sqlradacct {
1115 $self->cust_svc->seconds_since_sqlradacct(@_);
1118 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1120 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1121 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1122 TIMESTAMP_END (exclusive).
1124 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1125 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1130 #note: POD here, implementation in FS::cust_svc
1131 sub attribute_since_sqlradacct {
1133 $self->cust_svc->attribute_since_sqlradacct(@_);
1136 =item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
1138 Returns an array of hash references of this customers login history for the
1139 given time range. (document this better)
1143 sub get_session_history_sqlradacct {
1145 $self->cust_svc->get_session_history_sqlradacct(@_);
1150 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1156 if ( $self->usergroup ) {
1157 #when provisioning records, export callback runs in svc_Common.pm before
1158 #radius_usergroup records can be inserted...
1159 @{$self->usergroup};
1161 map { $_->groupname }
1162 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1166 =item clone_suspended
1168 Constructor used by FS::part_export::_export_suspend fallback. Document
1173 sub clone_suspended {
1175 my %hash = $self->hash;
1176 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1177 new FS::svc_acct \%hash;
1180 =item clone_kludge_unsuspend
1182 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1187 sub clone_kludge_unsuspend {
1189 my %hash = $self->hash;
1190 $hash{_password} = '';
1191 new FS::svc_acct \%hash;
1194 =item check_password
1196 Checks the supplied password against the (possibly encrypted) password in the
1197 database. Returns true for a sucessful authentication, false for no match.
1199 Currently supported encryptions are: classic DES crypt() and MD5
1203 sub check_password {
1204 my($self, $check_password) = @_;
1206 #remove old-style SUSPENDED kludge, they should be allowed to login to
1207 #self-service and pay up
1208 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1210 #eventually should check a "password-encoding" field
1211 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1213 } elsif ( length($password) < 13 ) { #plaintext
1214 $check_password eq $password;
1215 } elsif ( length($password) == 13 ) { #traditional DES crypt
1216 crypt($check_password, $password) eq $password;
1217 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1218 unix_md5_crypt($check_password, $password) eq $password;
1219 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1220 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1221 $self->svcnum. "\n";
1224 warn "Can't check password: Unrecognized encryption for svcnum ".
1225 $self->svcnum. "\n";
1245 use Mail::Internet 1.44;
1248 $opt{mimetype} ||= 'text/plain';
1249 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1251 $ENV{MAILADDRESS} = $opt{from};
1252 my $header = new Mail::Header ( [
1255 "Sender: $opt{from}",
1256 "Reply-To: $opt{from}",
1257 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1258 "Subject: $opt{subject}",
1259 "Content-Type: $opt{mimetype}",
1261 my $message = new Mail::Internet (
1262 'Header' => $header,
1263 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1266 $message->smtpsend( Host => $smtpmachine )
1267 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1268 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1271 =item check_and_rebuild_fuzzyfiles
1275 sub check_and_rebuild_fuzzyfiles {
1276 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1277 -e "$dir/svc_acct.username"
1278 or &rebuild_fuzzyfiles;
1281 =item rebuild_fuzzyfiles
1285 sub rebuild_fuzzyfiles {
1287 use Fcntl qw(:flock);
1289 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1293 open(USERNAMELOCK,">>$dir/svc_acct.username")
1294 or die "can't open $dir/svc_acct.username: $!";
1295 flock(USERNAMELOCK,LOCK_EX)
1296 or die "can't lock $dir/svc_acct.username: $!";
1298 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1300 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1301 or die "can't open $dir/svc_acct.username.tmp: $!";
1302 print USERNAMECACHE join("\n", @all_username), "\n";
1303 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1305 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1315 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1316 open(USERNAMECACHE,"<$dir/svc_acct.username")
1317 or die "can't open $dir/svc_acct.username: $!";
1318 my @array = map { chomp; $_; } <USERNAMECACHE>;
1319 close USERNAMECACHE;
1323 =item append_fuzzyfiles USERNAME
1327 sub append_fuzzyfiles {
1328 my $username = shift;
1330 &check_and_rebuild_fuzzyfiles;
1332 use Fcntl qw(:flock);
1334 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1336 open(USERNAME,">>$dir/svc_acct.username")
1337 or die "can't open $dir/svc_acct.username: $!";
1338 flock(USERNAME,LOCK_EX)
1339 or die "can't lock $dir/svc_acct.username: $!";
1341 print USERNAME "$username\n";
1343 flock(USERNAME,LOCK_UN)
1344 or die "can't unlock $dir/svc_acct.username: $!";
1352 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1356 sub radius_usergroup_selector {
1357 my $sel_groups = shift;
1358 my %sel_groups = map { $_=>1 } @$sel_groups;
1360 my $selectname = shift || 'radius_usergroup';
1363 my $sth = $dbh->prepare(
1364 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1365 ) or die $dbh->errstr;
1366 $sth->execute() or die $sth->errstr;
1367 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1371 function ${selectname}_doadd(object) {
1372 var myvalue = object.${selectname}_add.value;
1373 var optionName = new Option(myvalue,myvalue,false,true);
1374 var length = object.$selectname.length;
1375 object.$selectname.options[length] = optionName;
1376 object.${selectname}_add.value = "";
1379 <SELECT MULTIPLE NAME="$selectname">
1382 foreach my $group ( @all_groups ) {
1384 if ( $sel_groups{$group} ) {
1385 $html .= ' SELECTED';
1386 $sel_groups{$group} = 0;
1388 $html .= ">$group</OPTION>\n";
1390 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1391 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1393 $html .= '</SELECT>';
1395 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1396 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1405 The $recref stuff in sub check should be cleaned up.
1407 The suspend, unsuspend and cancel methods update the database, but not the
1408 current object. This is probably a bug as it's unexpected and
1411 radius_usergroup_selector? putting web ui components in here? they should
1412 probably live somewhere else...
1414 insertion of RADIUS group stuff in insert could be done with child_objects now
1415 (would probably clean up export of them too)
1419 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1420 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1421 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1422 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1423 schema.html from the base documentation.