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
18 use FS::UID qw( datasrc );
20 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
27 use FS::cust_main_invoice;
31 use FS::radius_usergroup;
34 use FS::Msgcat qw(gettext);
36 @ISA = qw( FS::svc_Common );
40 $me = '[FS::svc_acct]';
42 #ask FS::UID to run this stuff for us later
43 $FS::UID::callback{'FS::svc_acct'} = sub {
45 $dir_prefix = $conf->config('home');
46 @shells = $conf->config('shells');
47 $usernamemin = $conf->config('usernamemin') || 2;
48 $usernamemax = $conf->config('usernamemax');
49 $passwordmin = $conf->config('passwordmin') || 6;
50 $passwordmax = $conf->config('passwordmax') || 8;
51 $username_letter = $conf->exists('username-letter');
52 $username_letterfirst = $conf->exists('username-letterfirst');
53 $username_noperiod = $conf->exists('username-noperiod');
54 $username_nounderscore = $conf->exists('username-nounderscore');
55 $username_nodash = $conf->exists('username-nodash');
56 $username_uppercase = $conf->exists('username-uppercase');
57 $username_ampersand = $conf->exists('username-ampersand');
58 $mydomain = $conf->config('domain');
59 $dirhash = $conf->config('dirhash') || 0;
60 if ( $conf->exists('welcome_email') ) {
61 $welcome_template = new Text::Template (
63 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
64 ) or warn "can't create welcome email template: $Text::Template::ERROR";
65 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
66 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
67 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
69 $welcome_template = '';
71 $welcome_subject = '';
72 $welcome_mimetype = '';
74 $smtpmachine = $conf->config('smtpmachine');
75 $radius_password = $conf->config('radius-password') || 'Password';
76 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
79 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
80 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
84 my ( $hashref, $cache ) = @_;
85 if ( $hashref->{'svc_acct_svcnum'} ) {
86 $self->{'_domsvc'} = FS::svc_domain->new( {
87 'svcnum' => $hashref->{'domsvc'},
88 'domain' => $hashref->{'svc_acct_domain'},
89 'catchall' => $hashref->{'svc_acct_catchall'},
96 FS::svc_acct - Object methods for svc_acct records
102 $record = new FS::svc_acct \%hash;
103 $record = new FS::svc_acct { 'column' => 'value' };
105 $error = $record->insert;
107 $error = $new_record->replace($old_record);
109 $error = $record->delete;
111 $error = $record->check;
113 $error = $record->suspend;
115 $error = $record->unsuspend;
117 $error = $record->cancel;
119 %hash = $record->radius;
121 %hash = $record->radius_reply;
123 %hash = $record->radius_check;
125 $domain = $record->domain;
127 $svc_domain = $record->svc_domain;
129 $email = $record->email;
131 $seconds_since = $record->seconds_since($timestamp);
135 An FS::svc_acct object represents an account. FS::svc_acct inherits from
136 FS::svc_Common. The following fields are currently supported:
140 =item svcnum - primary key (assigned automatcially for new accounts)
144 =item _password - generated if blank
146 =item sec_phrase - security phrase
148 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
156 =item dir - set automatically if blank (and uid is not)
160 =item quota - (unimplementd)
162 =item slipip - IP address
166 =item domsvc - svcnum from svc_domain
168 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
178 Creates a new account. To add the account to the database, see L<"insert">.
182 sub table { 'svc_acct'; }
184 =item insert [ , OPTION => VALUE ... ]
186 Adds this account to the database. If there is an error, returns the error,
187 otherwise returns false.
189 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
190 defined. An FS::cust_svc record will be created and inserted.
192 The additional field I<usergroup> can optionally be defined; if so it should
193 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
194 sqlradius export only)
196 The additional field I<child_objects> can optionally be defined; if so it
197 should contain an arrayref of FS::tablename objects. They will have their
198 svcnum fields set and will be inserted after this record, but before any
201 Currently available options are: I<depend_jobnum>
203 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
204 jobnums), all provisioning jobs will have a dependancy on the supplied
205 jobnum(s) (they will not run until the specific job(s) complete(s)).
207 (TODOC: L<FS::queue> and L<freeside-queued>)
209 (TODOC: new exports!)
218 local $SIG{HUP} = 'IGNORE';
219 local $SIG{INT} = 'IGNORE';
220 local $SIG{QUIT} = 'IGNORE';
221 local $SIG{TERM} = 'IGNORE';
222 local $SIG{TSTP} = 'IGNORE';
223 local $SIG{PIPE} = 'IGNORE';
225 my $oldAutoCommit = $FS::UID::AutoCommit;
226 local $FS::UID::AutoCommit = 0;
229 $error = $self->check;
230 return $error if $error;
232 #no, duplicate checking just got a whole lot more complicated
233 #(perhaps keep this check with a config option to turn on?)
235 #return gettext('username_in_use'). ": ". $self->username
236 # if qsearchs( 'svc_acct', { 'username' => $self->username,
237 # 'domsvc' => $self->domsvc,
240 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
241 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
242 unless ( $cust_svc ) {
243 $dbh->rollback if $oldAutoCommit;
244 return "no cust_svc record found for svcnum ". $self->svcnum;
246 $self->pkgnum($cust_svc->pkgnum);
247 $self->svcpart($cust_svc->svcpart);
250 #new duplicate username checking
252 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
253 unless ( $part_svc ) {
254 $dbh->rollback if $oldAutoCommit;
255 return 'unknown svcpart '. $self->svcpart;
258 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
259 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
260 'domsvc' => $self->domsvc } );
262 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
263 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
264 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
269 if ( @dup_user || @dup_userdomain || @dup_uid ) {
270 my $exports = FS::part_export::export_info('svc_acct');
271 my %conflict_user_svcpart;
272 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
274 foreach my $part_export ( $part_svc->part_export ) {
276 #this will catch to the same exact export
277 my @svcparts = map { $_->svcpart }
278 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
280 #this will catch to exports w/same exporthost+type ???
281 #my @other_part_export = qsearch('part_export', {
282 # 'machine' => $part_export->machine,
283 # 'exporttype' => $part_export->exporttype,
285 #foreach my $other_part_export ( @other_part_export ) {
286 # push @svcparts, map { $_->svcpart }
287 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
290 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
291 #silly kludge to avoid uninitialized value errors
292 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
293 ? $exports->{$part_export->exporttype}{'nodomain'}
295 if ( $nodomain =~ /^Y/i ) {
296 $conflict_user_svcpart{$_} = $part_export->exportnum
299 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
304 foreach my $dup_user ( @dup_user ) {
305 my $dup_svcpart = $dup_user->cust_svc->svcpart;
306 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
307 $dbh->rollback if $oldAutoCommit;
308 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
309 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
313 foreach my $dup_userdomain ( @dup_userdomain ) {
314 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
315 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
316 $dbh->rollback if $oldAutoCommit;
317 return "duplicate username\@domain: conflicts with svcnum ".
318 $dup_userdomain->svcnum. " via exportnum ".
319 $conflict_userdomain_svcpart{$dup_svcpart};
323 foreach my $dup_uid ( @dup_uid ) {
324 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
325 if ( exists($conflict_user_svcpart{$dup_svcpart})
326 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
327 $dbh->rollback if $oldAutoCommit;
328 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
329 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
330 || $conflict_userdomain_svcpart{$dup_svcpart};
336 #see? i told you it was more complicated
339 $error = $self->SUPER::insert(
340 'jobnums' => \@jobnums,
341 'child_objects' => $self->child_objects,
345 $dbh->rollback if $oldAutoCommit;
349 if ( $self->usergroup ) {
350 foreach my $groupname ( @{$self->usergroup} ) {
351 my $radius_usergroup = new FS::radius_usergroup ( {
352 svcnum => $self->svcnum,
353 groupname => $groupname,
355 my $error = $radius_usergroup->insert;
357 $dbh->rollback if $oldAutoCommit;
363 #false laziness with sub replace (and cust_main)
364 my $queue = new FS::queue {
365 'svcnum' => $self->svcnum,
366 'job' => 'FS::svc_acct::append_fuzzyfiles'
368 $error = $queue->insert($self->username);
370 $dbh->rollback if $oldAutoCommit;
371 return "queueing job (transaction rolled back): $error";
374 my $cust_pkg = $self->cust_svc->cust_pkg;
377 my $cust_main = $cust_pkg->cust_main;
379 if ( $conf->exists('emailinvoiceauto') ) {
380 my @invoicing_list = $cust_main->invoicing_list;
381 push @invoicing_list, $self->email;
382 $cust_main->invoicing_list(\@invoicing_list);
387 if ( $welcome_template && $cust_pkg ) {
388 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
390 my $wqueue = new FS::queue {
391 'svcnum' => $self->svcnum,
392 'job' => 'FS::svc_acct::send_email'
394 my $error = $wqueue->insert(
396 'from' => $welcome_from,
397 'subject' => $welcome_subject,
398 'mimetype' => $welcome_mimetype,
399 'body' => $welcome_template->fill_in( HASH => {
400 'custnum' => $self->custnum,
401 'username' => $self->username,
402 'password' => $self->_password,
403 'first' => $cust_main->first,
404 'last' => $cust_main->getfield('last'),
405 'pkg' => $cust_pkg->part_pkg->pkg,
409 $dbh->rollback if $oldAutoCommit;
410 return "error queuing welcome email: $error";
413 if ( $options{'depend_jobnum'} ) {
414 warn "$me depend_jobnum found; adding to welcome email dependancies"
416 if ( ref($options{'depend_jobnum'}) ) {
417 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
418 "to welcome email dependancies"
420 push @jobnums, @{ $options{'depend_jobnum'} };
422 warn "$me adding job $options{'depend_jobnum'} ".
423 "to welcome email dependancies"
425 push @jobnums, $options{'depend_jobnum'};
429 foreach my $jobnum ( @jobnums ) {
430 my $error = $wqueue->depend_insert($jobnum);
432 $dbh->rollback if $oldAutoCommit;
433 return "error queuing welcome email job dependancy: $error";
443 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
449 Deletes this account from the database. If there is an error, returns the
450 error, otherwise returns false.
452 The corresponding FS::cust_svc record will be deleted as well.
454 (TODOC: new exports!)
461 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
462 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
463 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
466 return "can't delete system account" if $self->_check_system;
468 return "Can't delete an account which is a (svc_forward) source!"
469 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
471 return "Can't delete an account which is a (svc_forward) destination!"
472 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
474 return "Can't delete an account with (svc_www) web service!"
475 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
477 # what about records in session ? (they should refer to history table)
479 local $SIG{HUP} = 'IGNORE';
480 local $SIG{INT} = 'IGNORE';
481 local $SIG{QUIT} = 'IGNORE';
482 local $SIG{TERM} = 'IGNORE';
483 local $SIG{TSTP} = 'IGNORE';
484 local $SIG{PIPE} = 'IGNORE';
486 my $oldAutoCommit = $FS::UID::AutoCommit;
487 local $FS::UID::AutoCommit = 0;
490 foreach my $cust_main_invoice (
491 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
493 unless ( defined($cust_main_invoice) ) {
494 warn "WARNING: something's wrong with qsearch";
497 my %hash = $cust_main_invoice->hash;
498 $hash{'dest'} = $self->email;
499 my $new = new FS::cust_main_invoice \%hash;
500 my $error = $new->replace($cust_main_invoice);
502 $dbh->rollback if $oldAutoCommit;
507 foreach my $svc_domain (
508 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
510 my %hash = new FS::svc_domain->hash;
511 $hash{'catchall'} = '';
512 my $new = new FS::svc_domain \%hash;
513 my $error = $new->replace($svc_domain);
515 $dbh->rollback if $oldAutoCommit;
520 foreach my $radius_usergroup (
521 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
523 my $error = $radius_usergroup->delete;
525 $dbh->rollback if $oldAutoCommit;
530 my $error = $self->SUPER::delete;
532 $dbh->rollback if $oldAutoCommit;
536 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
540 =item replace OLD_RECORD
542 Replaces OLD_RECORD with this one in the database. If there is an error,
543 returns the error, otherwise returns false.
545 The additional field I<usergroup> can optionally be defined; if so it should
546 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
547 sqlradius export only)
552 my ( $new, $old ) = ( shift, shift );
554 warn "$me replacing $old with $new\n" if $DEBUG;
556 return "can't modify system account" if $old->_check_system;
558 return "Username in use"
559 if $old->username ne $new->username &&
560 qsearchs( 'svc_acct', { 'username' => $new->username,
561 'domsvc' => $new->domsvc,
564 #no warnings 'numeric'; #alas, a 5.006-ism
566 return "Can't change uid!" if $old->uid != $new->uid;
569 #change homdir when we change username
570 $new->setfield('dir', '') if $old->username ne $new->username;
572 local $SIG{HUP} = 'IGNORE';
573 local $SIG{INT} = 'IGNORE';
574 local $SIG{QUIT} = 'IGNORE';
575 local $SIG{TERM} = 'IGNORE';
576 local $SIG{TSTP} = 'IGNORE';
577 local $SIG{PIPE} = 'IGNORE';
579 my $oldAutoCommit = $FS::UID::AutoCommit;
580 local $FS::UID::AutoCommit = 0;
583 # redundant, but so $new->usergroup gets set
584 $error = $new->check;
585 return $error if $error;
587 $old->usergroup( [ $old->radius_groups ] );
588 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
589 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
590 if ( $new->usergroup ) {
591 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
592 my @newgroups = @{$new->usergroup};
593 foreach my $oldgroup ( @{$old->usergroup} ) {
594 if ( grep { $oldgroup eq $_ } @newgroups ) {
595 @newgroups = grep { $oldgroup ne $_ } @newgroups;
598 my $radius_usergroup = qsearchs('radius_usergroup', {
599 svcnum => $old->svcnum,
600 groupname => $oldgroup,
602 my $error = $radius_usergroup->delete;
604 $dbh->rollback if $oldAutoCommit;
605 return "error deleting radius_usergroup $oldgroup: $error";
609 foreach my $newgroup ( @newgroups ) {
610 my $radius_usergroup = new FS::radius_usergroup ( {
611 svcnum => $new->svcnum,
612 groupname => $newgroup,
614 my $error = $radius_usergroup->insert;
616 $dbh->rollback if $oldAutoCommit;
617 return "error adding radius_usergroup $newgroup: $error";
623 $error = $new->SUPER::replace($old);
625 $dbh->rollback if $oldAutoCommit;
626 return $error if $error;
629 if ( $new->username ne $old->username ) {
630 #false laziness with sub insert (and cust_main)
631 my $queue = new FS::queue {
632 'svcnum' => $new->svcnum,
633 'job' => 'FS::svc_acct::append_fuzzyfiles'
635 $error = $queue->insert($new->username);
637 $dbh->rollback if $oldAutoCommit;
638 return "queueing job (transaction rolled back): $error";
642 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
648 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
649 error, returns the error, otherwise returns false.
651 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
653 Calls any export-specific suspend hooks.
659 return "can't suspend system account" if $self->_check_system;
660 $self->SUPER::suspend;
665 Unsuspends this account by removing *SUSPENDED* from the password. If there is
666 an error, returns the error, otherwise returns false.
668 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
670 Calls any export-specific unsuspend hooks.
676 my %hash = $self->hash;
677 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
678 $hash{_password} = $1;
679 my $new = new FS::svc_acct ( \%hash );
680 my $error = $new->replace($self);
681 return $error if $error;
684 $self->SUPER::unsuspend;
689 Just returns false (no error) for now.
691 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
695 Checks all fields to make sure this is a valid service. If there is an error,
696 returns the error, otherwise returns false. Called by the insert and replace
699 Sets any fixed values; see L<FS::part_svc>.
706 my($recref) = $self->hashref;
708 my $x = $self->setfixed;
709 return $x unless ref($x);
712 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
714 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
717 my $error = $self->ut_numbern('svcnum')
718 #|| $self->ut_number('domsvc')
719 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
720 || $self->ut_textn('sec_phrase')
722 return $error if $error;
724 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
725 if ( $username_uppercase ) {
726 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
727 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
728 $recref->{username} = $1;
730 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
731 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
732 $recref->{username} = $1;
735 if ( $username_letterfirst ) {
736 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
737 } elsif ( $username_letter ) {
738 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
740 if ( $username_noperiod ) {
741 $recref->{username} =~ /\./ and return gettext('illegal_username');
743 if ( $username_nounderscore ) {
744 $recref->{username} =~ /_/ and return gettext('illegal_username');
746 if ( $username_nodash ) {
747 $recref->{username} =~ /\-/ and return gettext('illegal_username');
749 unless ( $username_ampersand ) {
750 $recref->{username} =~ /\&/ and return gettext('illegal_username');
753 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
754 $recref->{popnum} = $1;
755 return "Unknown popnum" unless
756 ! $recref->{popnum} ||
757 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
759 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
761 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
762 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
764 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
765 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
766 #not all systems use gid=uid
767 #you can set a fixed gid in part_svc
769 return "Only root can have uid 0"
770 if $recref->{uid} == 0
771 && $recref->{username} ne 'root'
772 && $recref->{username} ne 'toor';
775 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
776 or return "Illegal directory: ". $recref->{dir};
778 return "Illegal directory"
779 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
780 return "Illegal directory"
781 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
782 unless ( $recref->{dir} ) {
783 $recref->{dir} = $dir_prefix . '/';
784 if ( $dirhash > 0 ) {
785 for my $h ( 1 .. $dirhash ) {
786 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
788 } elsif ( $dirhash < 0 ) {
789 for my $h ( reverse $dirhash .. -1 ) {
790 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
793 $recref->{dir} .= $recref->{username};
797 unless ( $recref->{username} eq 'sync' ) {
798 if ( grep $_ eq $recref->{shell}, @shells ) {
799 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
801 return "Illegal shell \`". $self->shell. "\'; ".
802 $conf->dir. "/shells contains: @shells";
805 $recref->{shell} = '/bin/sync';
809 $recref->{gid} ne '' ?
810 return "Can't have gid without uid" : ( $recref->{gid}='' );
811 $recref->{dir} ne '' ?
812 return "Can't have directory without uid" : ( $recref->{dir}='' );
813 $recref->{shell} ne '' ?
814 return "Can't have shell without uid" : ( $recref->{shell}='' );
817 # $error = $self->ut_textn('finger');
818 # return $error if $error;
819 $self->getfield('finger') =~
820 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
821 or return "Illegal finger: ". $self->getfield('finger');
822 $self->setfield('finger', $1);
824 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
825 $recref->{quota} = $1;
827 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
828 if ( $recref->{slipip} eq '' ) {
829 $recref->{slipip} = '';
830 } elsif ( $recref->{slipip} eq '0e0' ) {
831 $recref->{slipip} = '0e0';
833 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
834 or return "Illegal slipip". $self->slipip;
835 $recref->{slipip} = $1;
840 #arbitrary RADIUS stuff; allow ut_textn for now
841 foreach ( grep /^radius_/, fields('svc_acct') ) {
845 #generate a password if it is blank
846 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
847 unless ( $recref->{_password} );
849 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
850 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
851 $recref->{_password} = $1.$3;
852 #uncomment this to encrypt password immediately upon entry, or run
853 #bin/crypt_pw in cron to give new users a window during which their
854 #password is available to techs, for faxing, etc. (also be aware of
856 #$recref->{password} = $1.
857 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
859 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
860 $recref->{_password} = $1.$3;
861 } elsif ( $recref->{_password} eq '*' ) {
862 $recref->{_password} = '*';
863 } elsif ( $recref->{_password} eq '!' ) {
864 $recref->{_password} = '!';
865 } elsif ( $recref->{_password} eq '!!' ) {
866 $recref->{_password} = '!!';
868 #return "Illegal password";
869 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
870 FS::Msgcat::_gettext('illegal_password_characters').
871 ": ". $recref->{_password};
883 scalar( grep { $self->username eq $_ || $self->email eq $_ }
884 $conf->config('system_usernames')
891 Depriciated, use radius_reply instead.
896 carp "FS::svc_acct::radius depriciated, use radius_reply";
902 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
903 reply attributes of this record.
905 Note that this is now the preferred method for reading RADIUS attributes -
906 accessing the columns directly is discouraged, as the column names are
907 expected to change in the future.
916 my($column, $attrib) = ($1, $2);
917 #$attrib =~ s/_/\-/g;
918 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
919 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
920 if ( $self->slipip && $self->slipip ne '0e0' ) {
921 $reply{$radius_ip} = $self->slipip;
928 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
929 check attributes of this record.
931 Note that this is now the preferred method for reading RADIUS attributes -
932 accessing the columns directly is discouraged, as the column names are
933 expected to change in the future.
939 my $password = $self->_password;
940 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
941 ( $pw_attrib => $self->_password,
944 my($column, $attrib) = ($1, $2);
945 #$attrib =~ s/_/\-/g;
946 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
947 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
953 Returns the domain associated with this account.
959 if ( $self->domsvc ) {
960 #$self->svc_domain->domain;
961 my $svc_domain = $self->svc_domain
962 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
965 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
971 Returns the FS::svc_domain record for this account's domain (see
980 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
985 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
991 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
996 Returns an email address associated with the account.
1002 $self->username. '@'. $self->domain;
1007 Returns an array of FS::acct_snarf records associated with the account.
1008 If the acct_snarf table does not exist or there are no associated records,
1009 an empty list is returned
1015 return () unless dbdef->table('acct_snarf');
1016 eval "use FS::acct_snarf;";
1018 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1021 =item seconds_since TIMESTAMP
1023 Returns the number of seconds this account has been online since TIMESTAMP,
1024 according to the session monitor (see L<FS::Session>).
1026 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1027 L<Time::Local> and L<Date::Parse> for conversion functions.
1031 #note: POD here, implementation in FS::cust_svc
1034 $self->cust_svc->seconds_since(@_);
1037 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1039 Returns the numbers of seconds this account has been online between
1040 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1041 external SQL radacct table, specified via sqlradius export. Sessions which
1042 started in the specified range but are still open are counted from session
1043 start to the end of the range (unless they are over 1 day old, in which case
1044 they are presumed missing their stop record and not counted). Also, sessions
1045 which end in the range but started earlier are counted from the start of the
1046 range to session end. Finally, sessions which start before the range but end
1047 after are counted for the entire range.
1049 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1050 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1055 #note: POD here, implementation in FS::cust_svc
1056 sub seconds_since_sqlradacct {
1058 $self->cust_svc->seconds_since_sqlradacct(@_);
1061 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1063 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1064 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1065 TIMESTAMP_END (exclusive).
1067 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1068 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1073 #note: POD here, implementation in FS::cust_svc
1074 sub attribute_since_sqlradacct {
1076 $self->cust_svc->attribute_since_sqlradacct(@_);
1080 =item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
1082 Returns an array of hash references of this customers login history for the
1083 given time range. (document this better)
1087 sub get_session_history_sqlradacct {
1089 $self->cust_svc->get_session_history_sqlradacct(@_);
1094 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1100 if ( $self->usergroup ) {
1101 #when provisioning records, export callback runs in svc_Common.pm before
1102 #radius_usergroup records can be inserted...
1103 @{$self->usergroup};
1105 map { $_->groupname }
1106 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1110 =item clone_suspended
1112 Constructor used by FS::part_export::_export_suspend fallback. Document
1117 sub clone_suspended {
1119 my %hash = $self->hash;
1120 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1121 new FS::svc_acct \%hash;
1124 =item clone_kludge_unsuspend
1126 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1131 sub clone_kludge_unsuspend {
1133 my %hash = $self->hash;
1134 $hash{_password} = '';
1135 new FS::svc_acct \%hash;
1152 use Mail::Internet 1.44;
1155 $opt{mimetype} ||= 'text/plain';
1156 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1158 $ENV{MAILADDRESS} = $opt{from};
1159 my $header = new Mail::Header ( [
1162 "Sender: $opt{from}",
1163 "Reply-To: $opt{from}",
1164 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1165 "Subject: $opt{subject}",
1166 "Content-Type: $opt{mimetype}",
1168 my $message = new Mail::Internet (
1169 'Header' => $header,
1170 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1173 $message->smtpsend( Host => $smtpmachine )
1174 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1175 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1178 =item check_and_rebuild_fuzzyfiles
1182 sub check_and_rebuild_fuzzyfiles {
1183 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1184 -e "$dir/svc_acct.username"
1185 or &rebuild_fuzzyfiles;
1188 =item rebuild_fuzzyfiles
1192 sub rebuild_fuzzyfiles {
1194 use Fcntl qw(:flock);
1196 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1200 open(USERNAMELOCK,">>$dir/svc_acct.username")
1201 or die "can't open $dir/svc_acct.username: $!";
1202 flock(USERNAMELOCK,LOCK_EX)
1203 or die "can't lock $dir/svc_acct.username: $!";
1205 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1207 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1208 or die "can't open $dir/svc_acct.username.tmp: $!";
1209 print USERNAMECACHE join("\n", @all_username), "\n";
1210 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1212 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1222 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1223 open(USERNAMECACHE,"<$dir/svc_acct.username")
1224 or die "can't open $dir/svc_acct.username: $!";
1225 my @array = map { chomp; $_; } <USERNAMECACHE>;
1226 close USERNAMECACHE;
1230 =item append_fuzzyfiles USERNAME
1234 sub append_fuzzyfiles {
1235 my $username = shift;
1237 &check_and_rebuild_fuzzyfiles;
1239 use Fcntl qw(:flock);
1241 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1243 open(USERNAME,">>$dir/svc_acct.username")
1244 or die "can't open $dir/svc_acct.username: $!";
1245 flock(USERNAME,LOCK_EX)
1246 or die "can't lock $dir/svc_acct.username: $!";
1248 print USERNAME "$username\n";
1250 flock(USERNAME,LOCK_UN)
1251 or die "can't unlock $dir/svc_acct.username: $!";
1259 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1263 sub radius_usergroup_selector {
1264 my $sel_groups = shift;
1265 my %sel_groups = map { $_=>1 } @$sel_groups;
1267 my $selectname = shift || 'radius_usergroup';
1270 my $sth = $dbh->prepare(
1271 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1272 ) or die $dbh->errstr;
1273 $sth->execute() or die $sth->errstr;
1274 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1278 function ${selectname}_doadd(object) {
1279 var myvalue = object.${selectname}_add.value;
1280 var optionName = new Option(myvalue,myvalue,false,true);
1281 var length = object.$selectname.length;
1282 object.$selectname.options[length] = optionName;
1283 object.${selectname}_add.value = "";
1286 <SELECT MULTIPLE NAME="$selectname">
1289 foreach my $group ( @all_groups ) {
1291 if ( $sel_groups{$group} ) {
1292 $html .= ' SELECTED';
1293 $sel_groups{$group} = 0;
1295 $html .= ">$group</OPTION>\n";
1297 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1298 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1300 $html .= '</SELECT>';
1302 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1303 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1312 The $recref stuff in sub check should be cleaned up.
1314 The suspend, unsuspend and cancel methods update the database, but not the
1315 current object. This is probably a bug as it's unexpected and
1318 radius_usergroup_selector? putting web ui components in here? they should
1319 probably live somewhere else...
1321 insertion of RADIUS group stuff in insert could be done with child_objects now
1322 (would probably clean up export of them too)
1326 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1327 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1328 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1329 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1330 schema.html from the base documentation.