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>.
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>.
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;
1138 =item check_password
1140 Checks the supplied password against the (possibly encrypted) password in the
1141 database. Returns true for a sucessful authentication, false for no match.
1143 Currently supported encryptions are: classic DES crypt() and MD5
1147 sub check_password {
1148 my($self, $check_password) = @_;
1150 #remove old-style SUSPENDED kludge, they should be allowed to login to
1151 #self-service and pay up
1152 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1154 #eventually should check a "password-encoding" field
1155 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1157 } elsif ( length($password) < 13 ) { #plaintext
1158 $check_password eq $password;
1159 } elsif ( length($password) == 13 ) { #traditional DES crypt
1160 crypt($check_password, $password) eq $password;
1161 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1162 unix_md5_crypt($check_password, $password) eq $password;
1163 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1164 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1165 $self->svcnum. "\n";
1168 warn "Can't check password: Unrecognized encryption for svcnum ".
1169 $self->svcnum. "\n";
1189 use Mail::Internet 1.44;
1192 $opt{mimetype} ||= 'text/plain';
1193 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1195 $ENV{MAILADDRESS} = $opt{from};
1196 my $header = new Mail::Header ( [
1199 "Sender: $opt{from}",
1200 "Reply-To: $opt{from}",
1201 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1202 "Subject: $opt{subject}",
1203 "Content-Type: $opt{mimetype}",
1205 my $message = new Mail::Internet (
1206 'Header' => $header,
1207 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1210 $message->smtpsend( Host => $smtpmachine )
1211 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1212 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1215 =item check_and_rebuild_fuzzyfiles
1219 sub check_and_rebuild_fuzzyfiles {
1220 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1221 -e "$dir/svc_acct.username"
1222 or &rebuild_fuzzyfiles;
1225 =item rebuild_fuzzyfiles
1229 sub rebuild_fuzzyfiles {
1231 use Fcntl qw(:flock);
1233 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1237 open(USERNAMELOCK,">>$dir/svc_acct.username")
1238 or die "can't open $dir/svc_acct.username: $!";
1239 flock(USERNAMELOCK,LOCK_EX)
1240 or die "can't lock $dir/svc_acct.username: $!";
1242 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1244 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1245 or die "can't open $dir/svc_acct.username.tmp: $!";
1246 print USERNAMECACHE join("\n", @all_username), "\n";
1247 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1249 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1259 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1260 open(USERNAMECACHE,"<$dir/svc_acct.username")
1261 or die "can't open $dir/svc_acct.username: $!";
1262 my @array = map { chomp; $_; } <USERNAMECACHE>;
1263 close USERNAMECACHE;
1267 =item append_fuzzyfiles USERNAME
1271 sub append_fuzzyfiles {
1272 my $username = shift;
1274 &check_and_rebuild_fuzzyfiles;
1276 use Fcntl qw(:flock);
1278 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1280 open(USERNAME,">>$dir/svc_acct.username")
1281 or die "can't open $dir/svc_acct.username: $!";
1282 flock(USERNAME,LOCK_EX)
1283 or die "can't lock $dir/svc_acct.username: $!";
1285 print USERNAME "$username\n";
1287 flock(USERNAME,LOCK_UN)
1288 or die "can't unlock $dir/svc_acct.username: $!";
1296 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1300 sub radius_usergroup_selector {
1301 my $sel_groups = shift;
1302 my %sel_groups = map { $_=>1 } @$sel_groups;
1304 my $selectname = shift || 'radius_usergroup';
1307 my $sth = $dbh->prepare(
1308 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1309 ) or die $dbh->errstr;
1310 $sth->execute() or die $sth->errstr;
1311 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1315 function ${selectname}_doadd(object) {
1316 var myvalue = object.${selectname}_add.value;
1317 var optionName = new Option(myvalue,myvalue,false,true);
1318 var length = object.$selectname.length;
1319 object.$selectname.options[length] = optionName;
1320 object.${selectname}_add.value = "";
1323 <SELECT MULTIPLE NAME="$selectname">
1326 foreach my $group ( @all_groups ) {
1328 if ( $sel_groups{$group} ) {
1329 $html .= ' SELECTED';
1330 $sel_groups{$group} = 0;
1332 $html .= ">$group</OPTION>\n";
1334 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1335 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1337 $html .= '</SELECT>';
1339 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1340 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1349 The $recref stuff in sub check should be cleaned up.
1351 The suspend, unsuspend and cancel methods update the database, but not the
1352 current object. This is probably a bug as it's unexpected and
1355 radius_usergroup_selector? putting web ui components in here? they should
1356 probably live somewhere else...
1358 insertion of RADIUS group stuff in insert could be done with child_objects now
1359 (would probably clean up export of them too)
1363 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1364 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1365 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1366 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1367 schema.html from the base documentation.