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
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 );
28 use FS::cust_main_invoice;
32 use FS::radius_usergroup;
35 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 $mydomain = $conf->config('domain');
60 $dirhash = $conf->config('dirhash') || 0;
61 if ( $conf->exists('welcome_email') ) {
62 $welcome_template = new Text::Template (
64 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
65 ) or warn "can't create welcome email template: $Text::Template::ERROR";
66 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
67 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
68 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
70 $welcome_template = '';
72 $welcome_subject = '';
73 $welcome_mimetype = '';
75 $smtpmachine = $conf->config('smtpmachine');
76 $radius_password = $conf->config('radius-password') || 'Password';
77 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
80 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
81 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
85 my ( $hashref, $cache ) = @_;
86 if ( $hashref->{'svc_acct_svcnum'} ) {
87 $self->{'_domsvc'} = FS::svc_domain->new( {
88 'svcnum' => $hashref->{'domsvc'},
89 'domain' => $hashref->{'svc_acct_domain'},
90 'catchall' => $hashref->{'svc_acct_catchall'},
97 FS::svc_acct - Object methods for svc_acct records
103 $record = new FS::svc_acct \%hash;
104 $record = new FS::svc_acct { 'column' => 'value' };
106 $error = $record->insert;
108 $error = $new_record->replace($old_record);
110 $error = $record->delete;
112 $error = $record->check;
114 $error = $record->suspend;
116 $error = $record->unsuspend;
118 $error = $record->cancel;
120 %hash = $record->radius;
122 %hash = $record->radius_reply;
124 %hash = $record->radius_check;
126 $domain = $record->domain;
128 $svc_domain = $record->svc_domain;
130 $email = $record->email;
132 $seconds_since = $record->seconds_since($timestamp);
136 An FS::svc_acct object represents an account. FS::svc_acct inherits from
137 FS::svc_Common. The following fields are currently supported:
141 =item svcnum - primary key (assigned automatcially for new accounts)
145 =item _password - generated if blank
147 =item sec_phrase - security phrase
149 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
157 =item dir - set automatically if blank (and uid is not)
161 =item quota - (unimplementd)
163 =item slipip - IP address
167 =item domsvc - svcnum from svc_domain
169 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
179 Creates a new account. To add the account to the database, see L<"insert">.
183 sub table { 'svc_acct'; }
185 =item insert [ , OPTION => VALUE ... ]
187 Adds this account to the database. If there is an error, returns the error,
188 otherwise returns false.
190 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
191 defined. An FS::cust_svc record will be created and inserted.
193 The additional field I<usergroup> can optionally be defined; if so it should
194 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
195 sqlradius export only)
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
202 Currently available options are: I<depend_jobnum>
204 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
205 jobnums), all provisioning jobs will have a dependancy on the supplied
206 jobnum(s) (they will not run until the specific job(s) complete(s)).
208 (TODOC: L<FS::queue> and L<freeside-queued>)
210 (TODOC: new exports!)
219 local $SIG{HUP} = 'IGNORE';
220 local $SIG{INT} = 'IGNORE';
221 local $SIG{QUIT} = 'IGNORE';
222 local $SIG{TERM} = 'IGNORE';
223 local $SIG{TSTP} = 'IGNORE';
224 local $SIG{PIPE} = 'IGNORE';
226 my $oldAutoCommit = $FS::UID::AutoCommit;
227 local $FS::UID::AutoCommit = 0;
230 $error = $self->check;
231 return $error if $error;
233 #no, duplicate checking just got a whole lot more complicated
234 #(perhaps keep this check with a config option to turn on?)
236 #return gettext('username_in_use'). ": ". $self->username
237 # if qsearchs( 'svc_acct', { 'username' => $self->username,
238 # 'domsvc' => $self->domsvc,
241 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
242 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
243 unless ( $cust_svc ) {
244 $dbh->rollback if $oldAutoCommit;
245 return "no cust_svc record found for svcnum ". $self->svcnum;
247 $self->pkgnum($cust_svc->pkgnum);
248 $self->svcpart($cust_svc->svcpart);
251 #new duplicate username checking
253 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
254 unless ( $part_svc ) {
255 $dbh->rollback if $oldAutoCommit;
256 return 'unknown svcpart '. $self->svcpart;
259 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
260 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
261 'domsvc' => $self->domsvc } );
263 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
264 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
265 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
270 if ( @dup_user || @dup_userdomain || @dup_uid ) {
271 my $exports = FS::part_export::export_info('svc_acct');
272 my %conflict_user_svcpart;
273 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
275 foreach my $part_export ( $part_svc->part_export ) {
277 #this will catch to the same exact export
278 my @svcparts = map { $_->svcpart }
279 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
281 #this will catch to exports w/same exporthost+type ???
282 #my @other_part_export = qsearch('part_export', {
283 # 'machine' => $part_export->machine,
284 # 'exporttype' => $part_export->exporttype,
286 #foreach my $other_part_export ( @other_part_export ) {
287 # push @svcparts, map { $_->svcpart }
288 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
291 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
292 #silly kludge to avoid uninitialized value errors
293 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
294 ? $exports->{$part_export->exporttype}{'nodomain'}
296 if ( $nodomain =~ /^Y/i ) {
297 $conflict_user_svcpart{$_} = $part_export->exportnum
300 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
305 foreach my $dup_user ( @dup_user ) {
306 my $dup_svcpart = $dup_user->cust_svc->svcpart;
307 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
308 $dbh->rollback if $oldAutoCommit;
309 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
310 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
314 foreach my $dup_userdomain ( @dup_userdomain ) {
315 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
316 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
317 $dbh->rollback if $oldAutoCommit;
318 return "duplicate username\@domain: conflicts with svcnum ".
319 $dup_userdomain->svcnum. " via exportnum ".
320 $conflict_userdomain_svcpart{$dup_svcpart};
324 foreach my $dup_uid ( @dup_uid ) {
325 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
326 if ( exists($conflict_user_svcpart{$dup_svcpart})
327 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
328 $dbh->rollback if $oldAutoCommit;
329 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
330 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
331 || $conflict_userdomain_svcpart{$dup_svcpart};
337 #see? i told you it was more complicated
340 $error = $self->SUPER::insert(
341 'jobnums' => \@jobnums,
342 'child_objects' => $self->child_objects,
346 $dbh->rollback if $oldAutoCommit;
350 if ( $self->usergroup ) {
351 foreach my $groupname ( @{$self->usergroup} ) {
352 my $radius_usergroup = new FS::radius_usergroup ( {
353 svcnum => $self->svcnum,
354 groupname => $groupname,
356 my $error = $radius_usergroup->insert;
358 $dbh->rollback if $oldAutoCommit;
364 #false laziness with sub replace (and cust_main)
365 my $queue = new FS::queue {
366 'svcnum' => $self->svcnum,
367 'job' => 'FS::svc_acct::append_fuzzyfiles'
369 $error = $queue->insert($self->username);
371 $dbh->rollback if $oldAutoCommit;
372 return "queueing job (transaction rolled back): $error";
375 my $cust_pkg = $self->cust_svc->cust_pkg;
378 my $cust_main = $cust_pkg->cust_main;
380 if ( $conf->exists('emailinvoiceauto') ) {
381 my @invoicing_list = $cust_main->invoicing_list;
382 push @invoicing_list, $self->email;
383 $cust_main->invoicing_list(\@invoicing_list);
388 if ( $welcome_template && $cust_pkg ) {
389 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
391 my $wqueue = new FS::queue {
392 'svcnum' => $self->svcnum,
393 'job' => 'FS::svc_acct::send_email'
395 my $error = $wqueue->insert(
397 'from' => $welcome_from,
398 'subject' => $welcome_subject,
399 'mimetype' => $welcome_mimetype,
400 'body' => $welcome_template->fill_in( HASH => {
401 'custnum' => $self->custnum,
402 'username' => $self->username,
403 'password' => $self->_password,
404 'first' => $cust_main->first,
405 'last' => $cust_main->getfield('last'),
406 'pkg' => $cust_pkg->part_pkg->pkg,
410 $dbh->rollback if $oldAutoCommit;
411 return "error queuing welcome email: $error";
414 if ( $options{'depend_jobnum'} ) {
415 warn "$me depend_jobnum found; adding to welcome email dependancies"
417 if ( ref($options{'depend_jobnum'}) ) {
418 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
419 "to welcome email dependancies"
421 push @jobnums, @{ $options{'depend_jobnum'} };
423 warn "$me adding job $options{'depend_jobnum'} ".
424 "to welcome email dependancies"
426 push @jobnums, $options{'depend_jobnum'};
430 foreach my $jobnum ( @jobnums ) {
431 my $error = $wqueue->depend_insert($jobnum);
433 $dbh->rollback if $oldAutoCommit;
434 return "error queuing welcome email job dependancy: $error";
444 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
450 Deletes this account from the database. If there is an error, returns the
451 error, otherwise returns false.
453 The corresponding FS::cust_svc record will be deleted as well.
455 (TODOC: new exports!)
462 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
463 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
464 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
467 return "can't delete system account" if $self->_check_system;
469 return "Can't delete an account which is a (svc_forward) source!"
470 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
472 return "Can't delete an account which is a (svc_forward) destination!"
473 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
475 return "Can't delete an account with (svc_www) web service!"
476 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
478 # what about records in session ? (they should refer to history table)
480 local $SIG{HUP} = 'IGNORE';
481 local $SIG{INT} = 'IGNORE';
482 local $SIG{QUIT} = 'IGNORE';
483 local $SIG{TERM} = 'IGNORE';
484 local $SIG{TSTP} = 'IGNORE';
485 local $SIG{PIPE} = 'IGNORE';
487 my $oldAutoCommit = $FS::UID::AutoCommit;
488 local $FS::UID::AutoCommit = 0;
491 foreach my $cust_main_invoice (
492 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
494 unless ( defined($cust_main_invoice) ) {
495 warn "WARNING: something's wrong with qsearch";
498 my %hash = $cust_main_invoice->hash;
499 $hash{'dest'} = $self->email;
500 my $new = new FS::cust_main_invoice \%hash;
501 my $error = $new->replace($cust_main_invoice);
503 $dbh->rollback if $oldAutoCommit;
508 foreach my $svc_domain (
509 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
511 my %hash = new FS::svc_domain->hash;
512 $hash{'catchall'} = '';
513 my $new = new FS::svc_domain \%hash;
514 my $error = $new->replace($svc_domain);
516 $dbh->rollback if $oldAutoCommit;
521 foreach my $radius_usergroup (
522 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
524 my $error = $radius_usergroup->delete;
526 $dbh->rollback if $oldAutoCommit;
531 my $error = $self->SUPER::delete;
533 $dbh->rollback if $oldAutoCommit;
537 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
541 =item replace OLD_RECORD
543 Replaces OLD_RECORD with this one in the database. If there is an error,
544 returns the error, otherwise returns false.
546 The additional field I<usergroup> can optionally be defined; if so it should
547 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
548 sqlradius export only)
553 my ( $new, $old ) = ( shift, shift );
555 warn "$me replacing $old with $new\n" if $DEBUG;
557 return "can't modify system account" if $old->_check_system;
559 return "Username in use"
560 if $old->username ne $new->username &&
561 qsearchs( 'svc_acct', { 'username' => $new->username,
562 'domsvc' => $new->domsvc,
565 #no warnings 'numeric'; #alas, a 5.006-ism
567 return "Can't change uid!" if $old->uid != $new->uid;
570 #change homdir when we change username
571 $new->setfield('dir', '') if $old->username ne $new->username;
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 # redundant, but so $new->usergroup gets set
585 $error = $new->check;
586 return $error if $error;
588 $old->usergroup( [ $old->radius_groups ] );
589 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
590 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
591 if ( $new->usergroup ) {
592 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
593 my @newgroups = @{$new->usergroup};
594 foreach my $oldgroup ( @{$old->usergroup} ) {
595 if ( grep { $oldgroup eq $_ } @newgroups ) {
596 @newgroups = grep { $oldgroup ne $_ } @newgroups;
599 my $radius_usergroup = qsearchs('radius_usergroup', {
600 svcnum => $old->svcnum,
601 groupname => $oldgroup,
603 my $error = $radius_usergroup->delete;
605 $dbh->rollback if $oldAutoCommit;
606 return "error deleting radius_usergroup $oldgroup: $error";
610 foreach my $newgroup ( @newgroups ) {
611 my $radius_usergroup = new FS::radius_usergroup ( {
612 svcnum => $new->svcnum,
613 groupname => $newgroup,
615 my $error = $radius_usergroup->insert;
617 $dbh->rollback if $oldAutoCommit;
618 return "error adding radius_usergroup $newgroup: $error";
624 $error = $new->SUPER::replace($old);
626 $dbh->rollback if $oldAutoCommit;
627 return $error if $error;
630 if ( $new->username ne $old->username ) {
631 #false laziness with sub insert (and cust_main)
632 my $queue = new FS::queue {
633 'svcnum' => $new->svcnum,
634 'job' => 'FS::svc_acct::append_fuzzyfiles'
636 $error = $queue->insert($new->username);
638 $dbh->rollback if $oldAutoCommit;
639 return "queueing job (transaction rolled back): $error";
643 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
649 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
650 error, returns the error, otherwise returns false.
652 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
654 Calls any export-specific suspend hooks.
660 return "can't suspend system account" if $self->_check_system;
661 $self->SUPER::suspend;
666 Unsuspends this account by removing *SUSPENDED* from the password. If there is
667 an error, returns the error, otherwise returns false.
669 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
671 Calls any export-specific unsuspend hooks.
677 my %hash = $self->hash;
678 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
679 $hash{_password} = $1;
680 my $new = new FS::svc_acct ( \%hash );
681 my $error = $new->replace($self);
682 return $error if $error;
685 $self->SUPER::unsuspend;
690 Just returns false (no error) for now.
692 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
696 Checks all fields to make sure this is a valid service. If there is an error,
697 returns the error, otherwise returns false. Called by the insert and replace
700 Sets any fixed values; see L<FS::part_svc>.
707 my($recref) = $self->hashref;
709 my $x = $self->setfixed;
710 return $x unless ref($x);
713 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
715 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
718 my $error = $self->ut_numbern('svcnum')
719 #|| $self->ut_number('domsvc')
720 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
721 || $self->ut_textn('sec_phrase')
723 return $error if $error;
725 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
726 if ( $username_uppercase ) {
727 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
728 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
729 $recref->{username} = $1;
731 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
732 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
733 $recref->{username} = $1;
736 if ( $username_letterfirst ) {
737 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
738 } elsif ( $username_letter ) {
739 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
741 if ( $username_noperiod ) {
742 $recref->{username} =~ /\./ and return gettext('illegal_username');
744 if ( $username_nounderscore ) {
745 $recref->{username} =~ /_/ and return gettext('illegal_username');
747 if ( $username_nodash ) {
748 $recref->{username} =~ /\-/ and return gettext('illegal_username');
750 unless ( $username_ampersand ) {
751 $recref->{username} =~ /\&/ and return gettext('illegal_username');
754 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
755 $recref->{popnum} = $1;
756 return "Unknown popnum" unless
757 ! $recref->{popnum} ||
758 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
760 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
762 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
763 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
765 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
766 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
767 #not all systems use gid=uid
768 #you can set a fixed gid in part_svc
770 return "Only root can have uid 0"
771 if $recref->{uid} == 0
772 && $recref->{username} ne 'root'
773 && $recref->{username} ne 'toor';
776 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
777 or return "Illegal directory: ". $recref->{dir};
779 return "Illegal directory"
780 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
781 return "Illegal directory"
782 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
783 unless ( $recref->{dir} ) {
784 $recref->{dir} = $dir_prefix . '/';
785 if ( $dirhash > 0 ) {
786 for my $h ( 1 .. $dirhash ) {
787 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
789 } elsif ( $dirhash < 0 ) {
790 for my $h ( reverse $dirhash .. -1 ) {
791 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
794 $recref->{dir} .= $recref->{username};
798 unless ( $recref->{username} eq 'sync' ) {
799 if ( grep $_ eq $recref->{shell}, @shells ) {
800 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
802 return "Illegal shell \`". $self->shell. "\'; ".
803 $conf->dir. "/shells contains: @shells";
806 $recref->{shell} = '/bin/sync';
810 $recref->{gid} ne '' ?
811 return "Can't have gid without uid" : ( $recref->{gid}='' );
812 $recref->{dir} ne '' ?
813 return "Can't have directory without uid" : ( $recref->{dir}='' );
814 $recref->{shell} ne '' ?
815 return "Can't have shell without uid" : ( $recref->{shell}='' );
818 # $error = $self->ut_textn('finger');
819 # return $error if $error;
820 $self->getfield('finger') =~
821 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
822 or return "Illegal finger: ". $self->getfield('finger');
823 $self->setfield('finger', $1);
825 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
826 $recref->{quota} = $1;
828 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
829 if ( $recref->{slipip} eq '' ) {
830 $recref->{slipip} = '';
831 } elsif ( $recref->{slipip} eq '0e0' ) {
832 $recref->{slipip} = '0e0';
834 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
835 or return "Illegal slipip". $self->slipip;
836 $recref->{slipip} = $1;
841 #arbitrary RADIUS stuff; allow ut_textn for now
842 foreach ( grep /^radius_/, fields('svc_acct') ) {
846 #generate a password if it is blank
847 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
848 unless ( $recref->{_password} );
850 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
851 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
852 $recref->{_password} = $1.$3;
853 #uncomment this to encrypt password immediately upon entry, or run
854 #bin/crypt_pw in cron to give new users a window during which their
855 #password is available to techs, for faxing, etc. (also be aware of
857 #$recref->{password} = $1.
858 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
860 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
861 $recref->{_password} = $1.$3;
862 } elsif ( $recref->{_password} eq '*' ) {
863 $recref->{_password} = '*';
864 } elsif ( $recref->{_password} eq '!' ) {
865 $recref->{_password} = '!';
866 } elsif ( $recref->{_password} eq '!!' ) {
867 $recref->{_password} = '!!';
869 #return "Illegal password";
870 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
871 FS::Msgcat::_gettext('illegal_password_characters').
872 ": ". $recref->{_password};
884 scalar( grep { $self->username eq $_ || $self->email eq $_ }
885 $conf->config('system_usernames')
892 Depriciated, use radius_reply instead.
897 carp "FS::svc_acct::radius depriciated, use radius_reply";
903 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
904 reply attributes of this record.
906 Note that this is now the preferred method for reading RADIUS attributes -
907 accessing the columns directly is discouraged, as the column names are
908 expected to change in the future.
917 my($column, $attrib) = ($1, $2);
918 #$attrib =~ s/_/\-/g;
919 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
920 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
921 if ( $self->slipip && $self->slipip ne '0e0' ) {
922 $reply{$radius_ip} = $self->slipip;
929 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
930 check attributes of this record.
932 Note that this is now the preferred method for reading RADIUS attributes -
933 accessing the columns directly is discouraged, as the column names are
934 expected to change in the future.
940 my $password = $self->_password;
941 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
942 ( $pw_attrib => $self->_password,
945 my($column, $attrib) = ($1, $2);
946 #$attrib =~ s/_/\-/g;
947 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
948 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
954 Returns the domain associated with this account.
960 if ( $self->domsvc ) {
961 #$self->svc_domain->domain;
962 my $svc_domain = $self->svc_domain
963 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
966 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
972 Returns the FS::svc_domain record for this account's domain (see
981 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
986 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
992 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
997 Returns an email address associated with the account.
1003 $self->username. '@'. $self->domain;
1008 Returns an array of FS::acct_snarf records associated with the account.
1009 If the acct_snarf table does not exist or there are no associated records,
1010 an empty list is returned
1016 return () unless dbdef->table('acct_snarf');
1017 eval "use FS::acct_snarf;";
1019 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1022 =item seconds_since TIMESTAMP
1024 Returns the number of seconds this account has been online since TIMESTAMP,
1025 according to the session monitor (see L<FS::Session>).
1027 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1028 L<Time::Local> and L<Date::Parse> for conversion functions.
1032 #note: POD here, implementation in FS::cust_svc
1035 $self->cust_svc->seconds_since(@_);
1038 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1040 Returns the numbers of seconds this account has been online between
1041 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1042 external SQL radacct table, specified via sqlradius export. Sessions which
1043 started in the specified range but are still open are counted from session
1044 start to the end of the range (unless they are over 1 day old, in which case
1045 they are presumed missing their stop record and not counted). Also, sessions
1046 which end in the range but started earlier are counted from the start of the
1047 range to session end. Finally, sessions which start before the range but end
1048 after are counted for the entire range.
1050 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1051 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1056 #note: POD here, implementation in FS::cust_svc
1057 sub seconds_since_sqlradacct {
1059 $self->cust_svc->seconds_since_sqlradacct(@_);
1062 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1064 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1065 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1066 TIMESTAMP_END (exclusive).
1068 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1069 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1074 #note: POD here, implementation in FS::cust_svc
1075 sub attribute_since_sqlradacct {
1077 $self->cust_svc->attribute_since_sqlradacct(@_);
1081 =item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
1083 Returns an array of hash references of this customers login history for the
1084 given time range. (document this better)
1088 sub get_session_history_sqlradacct {
1090 $self->cust_svc->get_session_history_sqlradacct(@_);
1095 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1101 if ( $self->usergroup ) {
1102 #when provisioning records, export callback runs in svc_Common.pm before
1103 #radius_usergroup records can be inserted...
1104 @{$self->usergroup};
1106 map { $_->groupname }
1107 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1111 =item clone_suspended
1113 Constructor used by FS::part_export::_export_suspend fallback. Document
1118 sub clone_suspended {
1120 my %hash = $self->hash;
1121 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1122 new FS::svc_acct \%hash;
1125 =item clone_kludge_unsuspend
1127 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1132 sub clone_kludge_unsuspend {
1134 my %hash = $self->hash;
1135 $hash{_password} = '';
1136 new FS::svc_acct \%hash;
1139 =item check_password
1141 Checks the supplied password against the (possibly encrypted) password in the
1142 database. Returns true for a sucessful authentication, false for no match.
1144 Currently supported encryptions are: classic DES crypt() and MD5
1148 sub check_password {
1149 my($self, $check_password) = @_;
1151 #remove old-style SUSPENDED kludge, they should be allowed to login to
1152 #self-service and pay up
1153 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1155 #eventually should check a "password-encoding" field
1156 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1158 } elsif ( length($password) < 13 ) { #plaintext
1159 $check_password eq $password;
1160 } elsif ( length($password) == 13 ) { #traditional DES crypt
1161 crypt($check_password, $password) eq $password;
1162 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1163 unix_md5_crypt($check_password, $password) eq $password;
1164 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1165 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1166 $self->svcnum. "\n";
1169 warn "Can't check password: Unrecognized encryption for svcnum ".
1170 $self->svcnum. "\n";
1190 use Mail::Internet 1.44;
1193 $opt{mimetype} ||= 'text/plain';
1194 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1196 $ENV{MAILADDRESS} = $opt{from};
1197 my $header = new Mail::Header ( [
1200 "Sender: $opt{from}",
1201 "Reply-To: $opt{from}",
1202 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1203 "Subject: $opt{subject}",
1204 "Content-Type: $opt{mimetype}",
1206 my $message = new Mail::Internet (
1207 'Header' => $header,
1208 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1211 $message->smtpsend( Host => $smtpmachine )
1212 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1213 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1216 =item check_and_rebuild_fuzzyfiles
1220 sub check_and_rebuild_fuzzyfiles {
1221 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1222 -e "$dir/svc_acct.username"
1223 or &rebuild_fuzzyfiles;
1226 =item rebuild_fuzzyfiles
1230 sub rebuild_fuzzyfiles {
1232 use Fcntl qw(:flock);
1234 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1238 open(USERNAMELOCK,">>$dir/svc_acct.username")
1239 or die "can't open $dir/svc_acct.username: $!";
1240 flock(USERNAMELOCK,LOCK_EX)
1241 or die "can't lock $dir/svc_acct.username: $!";
1243 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1245 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1246 or die "can't open $dir/svc_acct.username.tmp: $!";
1247 print USERNAMECACHE join("\n", @all_username), "\n";
1248 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1250 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1260 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1261 open(USERNAMECACHE,"<$dir/svc_acct.username")
1262 or die "can't open $dir/svc_acct.username: $!";
1263 my @array = map { chomp; $_; } <USERNAMECACHE>;
1264 close USERNAMECACHE;
1268 =item append_fuzzyfiles USERNAME
1272 sub append_fuzzyfiles {
1273 my $username = shift;
1275 &check_and_rebuild_fuzzyfiles;
1277 use Fcntl qw(:flock);
1279 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1281 open(USERNAME,">>$dir/svc_acct.username")
1282 or die "can't open $dir/svc_acct.username: $!";
1283 flock(USERNAME,LOCK_EX)
1284 or die "can't lock $dir/svc_acct.username: $!";
1286 print USERNAME "$username\n";
1288 flock(USERNAME,LOCK_UN)
1289 or die "can't unlock $dir/svc_acct.username: $!";
1297 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1301 sub radius_usergroup_selector {
1302 my $sel_groups = shift;
1303 my %sel_groups = map { $_=>1 } @$sel_groups;
1305 my $selectname = shift || 'radius_usergroup';
1308 my $sth = $dbh->prepare(
1309 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1310 ) or die $dbh->errstr;
1311 $sth->execute() or die $sth->errstr;
1312 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1316 function ${selectname}_doadd(object) {
1317 var myvalue = object.${selectname}_add.value;
1318 var optionName = new Option(myvalue,myvalue,false,true);
1319 var length = object.$selectname.length;
1320 object.$selectname.options[length] = optionName;
1321 object.${selectname}_add.value = "";
1324 <SELECT MULTIPLE NAME="$selectname">
1327 foreach my $group ( @all_groups ) {
1329 if ( $sel_groups{$group} ) {
1330 $html .= ' SELECTED';
1331 $sel_groups{$group} = 0;
1333 $html .= ">$group</OPTION>\n";
1335 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1336 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1338 $html .= '</SELECT>';
1340 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1341 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1350 The $recref stuff in sub check should be cleaned up.
1352 The suspend, unsuspend and cancel methods update the database, but not the
1353 current object. This is probably a bug as it's unexpected and
1356 radius_usergroup_selector? putting web ui components in here? they should
1357 probably live somewhere else...
1359 insertion of RADIUS group stuff in insert could be done with child_objects now
1360 (would probably clean up export of them too)
1364 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1365 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1366 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1367 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1368 schema.html from the base documentation.