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);
39 @ISA = qw( FS::svc_Common );
43 $me = '[FS::svc_acct]';
45 #ask FS::UID to run this stuff for us later
46 $FS::UID::callback{'FS::svc_acct'} = sub {
48 $dir_prefix = $conf->config('home');
49 @shells = $conf->config('shells');
50 $usernamemin = $conf->config('usernamemin') || 2;
51 $usernamemax = $conf->config('usernamemax');
52 $passwordmin = $conf->config('passwordmin') || 6;
53 $passwordmax = $conf->config('passwordmax') || 8;
54 $username_letter = $conf->exists('username-letter');
55 $username_letterfirst = $conf->exists('username-letterfirst');
56 $username_noperiod = $conf->exists('username-noperiod');
57 $username_nounderscore = $conf->exists('username-nounderscore');
58 $username_nodash = $conf->exists('username-nodash');
59 $username_uppercase = $conf->exists('username-uppercase');
60 $username_ampersand = $conf->exists('username-ampersand');
61 $mydomain = $conf->config('domain');
62 $dirhash = $conf->config('dirhash') || 0;
63 if ( $conf->exists('welcome_email') ) {
64 $welcome_template = new Text::Template (
66 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
67 ) or warn "can't create welcome email template: $Text::Template::ERROR";
68 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
69 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
70 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
72 $welcome_template = '';
74 $welcome_subject = '';
75 $welcome_mimetype = '';
77 $smtpmachine = $conf->config('smtpmachine');
78 $radius_password = $conf->config('radius-password') || 'Password';
79 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
82 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
83 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
87 my ( $hashref, $cache ) = @_;
88 if ( $hashref->{'svc_acct_svcnum'} ) {
89 $self->{'_domsvc'} = FS::svc_domain->new( {
90 'svcnum' => $hashref->{'domsvc'},
91 'domain' => $hashref->{'svc_acct_domain'},
92 'catchall' => $hashref->{'svc_acct_catchall'},
99 FS::svc_acct - Object methods for svc_acct records
105 $record = new FS::svc_acct \%hash;
106 $record = new FS::svc_acct { 'column' => 'value' };
108 $error = $record->insert;
110 $error = $new_record->replace($old_record);
112 $error = $record->delete;
114 $error = $record->check;
116 $error = $record->suspend;
118 $error = $record->unsuspend;
120 $error = $record->cancel;
122 %hash = $record->radius;
124 %hash = $record->radius_reply;
126 %hash = $record->radius_check;
128 $domain = $record->domain;
130 $svc_domain = $record->svc_domain;
132 $email = $record->email;
134 $seconds_since = $record->seconds_since($timestamp);
138 An FS::svc_acct object represents an account. FS::svc_acct inherits from
139 FS::svc_Common. The following fields are currently supported:
143 =item svcnum - primary key (assigned automatcially for new accounts)
147 =item _password - generated if blank
149 =item sec_phrase - security phrase
151 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
159 =item dir - set automatically if blank (and uid is not)
163 =item quota - (unimplementd)
165 =item slipip - IP address
169 =item domsvc - svcnum from svc_domain
171 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
181 Creates a new account. To add the account to the database, see L<"insert">.
185 sub table { 'svc_acct'; }
187 =item insert [ , OPTION => VALUE ... ]
189 Adds this account to the database. If there is an error, returns the error,
190 otherwise returns false.
192 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
193 defined. An FS::cust_svc record will be created and inserted.
195 The additional field I<usergroup> can optionally be defined; if so it should
196 contain an arrayref of group names. See L<FS::radius_usergroup>.
198 The additional field I<child_objects> can optionally be defined; if so it
199 should contain an arrayref of FS::tablename objects. They will have their
200 svcnum fields set and will be inserted after this record, but before any
203 Currently available options are: I<depend_jobnum>
205 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
206 jobnums), all provisioning jobs will have a dependancy on the supplied
207 jobnum(s) (they will not run until the specific job(s) complete(s)).
209 (TODOC: L<FS::queue> and L<freeside-queued>)
211 (TODOC: new exports!)
220 local $SIG{HUP} = 'IGNORE';
221 local $SIG{INT} = 'IGNORE';
222 local $SIG{QUIT} = 'IGNORE';
223 local $SIG{TERM} = 'IGNORE';
224 local $SIG{TSTP} = 'IGNORE';
225 local $SIG{PIPE} = 'IGNORE';
227 my $oldAutoCommit = $FS::UID::AutoCommit;
228 local $FS::UID::AutoCommit = 0;
231 $error = $self->check;
232 return $error if $error;
234 #no, duplicate checking just got a whole lot more complicated
235 #(perhaps keep this check with a config option to turn on?)
237 #return gettext('username_in_use'). ": ". $self->username
238 # if qsearchs( 'svc_acct', { 'username' => $self->username,
239 # 'domsvc' => $self->domsvc,
242 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
243 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
244 unless ( $cust_svc ) {
245 $dbh->rollback if $oldAutoCommit;
246 return "no cust_svc record found for svcnum ". $self->svcnum;
248 $self->pkgnum($cust_svc->pkgnum);
249 $self->svcpart($cust_svc->svcpart);
252 #new duplicate username/username@domain/uid checking
254 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
255 unless ( $part_svc ) {
256 $dbh->rollback if $oldAutoCommit;
257 return 'unknown svcpart '. $self->svcpart;
260 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
261 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
262 'domsvc' => $self->domsvc } );
264 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
265 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
266 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
271 if ( @dup_user || @dup_userdomain || @dup_uid ) {
272 my $exports = FS::part_export::export_info('svc_acct');
273 my %conflict_user_svcpart;
274 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
276 foreach my $part_export ( $part_svc->part_export ) {
278 #this will catch to the same exact export
279 my @svcparts = map { $_->svcpart } $part_export->export_svc;
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->svcnum } );
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>.
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 if ( $self->getfield('finger') eq '' ) {
821 my $cust_pkg = $self->svcnum
822 ? $self->cust_svc->cust_pkg
823 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
825 my $cust_main = $cust_pkg->cust_main;
826 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
829 $self->getfield('finger') =~
830 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
831 or return "Illegal finger: ". $self->getfield('finger');
832 $self->setfield('finger', $1);
834 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
835 $recref->{quota} = $1;
837 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
838 if ( $recref->{slipip} eq '' ) {
839 $recref->{slipip} = '';
840 } elsif ( $recref->{slipip} eq '0e0' ) {
841 $recref->{slipip} = '0e0';
843 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
844 or return "Illegal slipip". $self->slipip;
845 $recref->{slipip} = $1;
850 #arbitrary RADIUS stuff; allow ut_textn for now
851 foreach ( grep /^radius_/, fields('svc_acct') ) {
855 #generate a password if it is blank
856 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
857 unless ( $recref->{_password} );
859 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
860 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
861 $recref->{_password} = $1.$3;
862 #uncomment this to encrypt password immediately upon entry, or run
863 #bin/crypt_pw in cron to give new users a window during which their
864 #password is available to techs, for faxing, etc. (also be aware of
866 #$recref->{password} = $1.
867 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
869 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
870 $recref->{_password} = $1.$3;
871 } elsif ( $recref->{_password} eq '*' ) {
872 $recref->{_password} = '*';
873 } elsif ( $recref->{_password} eq '!' ) {
874 $recref->{_password} = '!';
875 } elsif ( $recref->{_password} eq '!!' ) {
876 $recref->{_password} = '!!';
878 #return "Illegal password";
879 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
880 FS::Msgcat::_gettext('illegal_password_characters').
881 ": ". $recref->{_password};
893 scalar( grep { $self->username eq $_ || $self->email eq $_ }
894 $conf->config('system_usernames')
901 Depriciated, use radius_reply instead.
906 carp "FS::svc_acct::radius depriciated, use radius_reply";
912 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
913 reply attributes of this record.
915 Note that this is now the preferred method for reading RADIUS attributes -
916 accessing the columns directly is discouraged, as the column names are
917 expected to change in the future.
926 my($column, $attrib) = ($1, $2);
927 #$attrib =~ s/_/\-/g;
928 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
929 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
930 if ( $self->slipip && $self->slipip ne '0e0' ) {
931 $reply{$radius_ip} = $self->slipip;
938 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
939 check attributes of this record.
941 Note that this is now the preferred method for reading RADIUS attributes -
942 accessing the columns directly is discouraged, as the column names are
943 expected to change in the future.
949 my $password = $self->_password;
950 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
951 ( $pw_attrib => $self->_password,
954 my($column, $attrib) = ($1, $2);
955 #$attrib =~ s/_/\-/g;
956 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
957 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
963 Returns the domain associated with this account.
969 if ( $self->domsvc ) {
970 #$self->svc_domain->domain;
971 my $svc_domain = $self->svc_domain
972 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
975 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
981 Returns the FS::svc_domain record for this account's domain (see
990 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
995 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1001 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
1006 Returns an email address associated with the account.
1012 $self->username. '@'. $self->domain;
1017 Returns an array of FS::acct_snarf records associated with the account.
1018 If the acct_snarf table does not exist or there are no associated records,
1019 an empty list is returned
1025 return () unless dbdef->table('acct_snarf');
1026 eval "use FS::acct_snarf;";
1028 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1031 =item seconds_since TIMESTAMP
1033 Returns the number of seconds this account has been online since TIMESTAMP,
1034 according to the session monitor (see L<FS::Session>).
1036 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1037 L<Time::Local> and L<Date::Parse> for conversion functions.
1041 #note: POD here, implementation in FS::cust_svc
1044 $self->cust_svc->seconds_since(@_);
1047 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1049 Returns the numbers of seconds this account has been online between
1050 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1051 external SQL radacct table, specified via sqlradius export. Sessions which
1052 started in the specified range but are still open are counted from session
1053 start to the end of the range (unless they are over 1 day old, in which case
1054 they are presumed missing their stop record and not counted). Also, sessions
1055 which end in the range but started earlier are counted from the start of the
1056 range to session end. Finally, sessions which start before the range but end
1057 after are counted for the entire range.
1059 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1060 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1065 #note: POD here, implementation in FS::cust_svc
1066 sub seconds_since_sqlradacct {
1068 $self->cust_svc->seconds_since_sqlradacct(@_);
1071 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1073 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1074 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1075 TIMESTAMP_END (exclusive).
1077 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1078 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1083 #note: POD here, implementation in FS::cust_svc
1084 sub attribute_since_sqlradacct {
1086 $self->cust_svc->attribute_since_sqlradacct(@_);
1090 =item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
1092 Returns an array of hash references of this customers login history for the
1093 given time range. (document this better)
1097 sub get_session_history_sqlradacct {
1099 $self->cust_svc->get_session_history_sqlradacct(@_);
1104 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1110 if ( $self->usergroup ) {
1111 #when provisioning records, export callback runs in svc_Common.pm before
1112 #radius_usergroup records can be inserted...
1113 @{$self->usergroup};
1115 map { $_->groupname }
1116 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1120 =item clone_suspended
1122 Constructor used by FS::part_export::_export_suspend fallback. Document
1127 sub clone_suspended {
1129 my %hash = $self->hash;
1130 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1131 new FS::svc_acct \%hash;
1134 =item clone_kludge_unsuspend
1136 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1141 sub clone_kludge_unsuspend {
1143 my %hash = $self->hash;
1144 $hash{_password} = '';
1145 new FS::svc_acct \%hash;
1148 =item check_password
1150 Checks the supplied password against the (possibly encrypted) password in the
1151 database. Returns true for a sucessful authentication, false for no match.
1153 Currently supported encryptions are: classic DES crypt() and MD5
1157 sub check_password {
1158 my($self, $check_password) = @_;
1160 #remove old-style SUSPENDED kludge, they should be allowed to login to
1161 #self-service and pay up
1162 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1164 #eventually should check a "password-encoding" field
1165 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1167 } elsif ( length($password) < 13 ) { #plaintext
1168 $check_password eq $password;
1169 } elsif ( length($password) == 13 ) { #traditional DES crypt
1170 crypt($check_password, $password) eq $password;
1171 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1172 unix_md5_crypt($check_password, $password) eq $password;
1173 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1174 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1175 $self->svcnum. "\n";
1178 warn "Can't check password: Unrecognized encryption for svcnum ".
1179 $self->svcnum. "\n";
1199 use Mail::Internet 1.44;
1202 $opt{mimetype} ||= 'text/plain';
1203 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1205 $ENV{MAILADDRESS} = $opt{from};
1206 my $header = new Mail::Header ( [
1209 "Sender: $opt{from}",
1210 "Reply-To: $opt{from}",
1211 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1212 "Subject: $opt{subject}",
1213 "Content-Type: $opt{mimetype}",
1215 my $message = new Mail::Internet (
1216 'Header' => $header,
1217 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1220 $message->smtpsend( Host => $smtpmachine )
1221 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1222 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1225 =item check_and_rebuild_fuzzyfiles
1229 sub check_and_rebuild_fuzzyfiles {
1230 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1231 -e "$dir/svc_acct.username"
1232 or &rebuild_fuzzyfiles;
1235 =item rebuild_fuzzyfiles
1239 sub rebuild_fuzzyfiles {
1241 use Fcntl qw(:flock);
1243 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1247 open(USERNAMELOCK,">>$dir/svc_acct.username")
1248 or die "can't open $dir/svc_acct.username: $!";
1249 flock(USERNAMELOCK,LOCK_EX)
1250 or die "can't lock $dir/svc_acct.username: $!";
1252 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1254 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1255 or die "can't open $dir/svc_acct.username.tmp: $!";
1256 print USERNAMECACHE join("\n", @all_username), "\n";
1257 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1259 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1269 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1270 open(USERNAMECACHE,"<$dir/svc_acct.username")
1271 or die "can't open $dir/svc_acct.username: $!";
1272 my @array = map { chomp; $_; } <USERNAMECACHE>;
1273 close USERNAMECACHE;
1277 =item append_fuzzyfiles USERNAME
1281 sub append_fuzzyfiles {
1282 my $username = shift;
1284 &check_and_rebuild_fuzzyfiles;
1286 use Fcntl qw(:flock);
1288 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1290 open(USERNAME,">>$dir/svc_acct.username")
1291 or die "can't open $dir/svc_acct.username: $!";
1292 flock(USERNAME,LOCK_EX)
1293 or die "can't lock $dir/svc_acct.username: $!";
1295 print USERNAME "$username\n";
1297 flock(USERNAME,LOCK_UN)
1298 or die "can't unlock $dir/svc_acct.username: $!";
1306 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1310 sub radius_usergroup_selector {
1311 my $sel_groups = shift;
1312 my %sel_groups = map { $_=>1 } @$sel_groups;
1314 my $selectname = shift || 'radius_usergroup';
1317 my $sth = $dbh->prepare(
1318 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1319 ) or die $dbh->errstr;
1320 $sth->execute() or die $sth->errstr;
1321 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1325 function ${selectname}_doadd(object) {
1326 var myvalue = object.${selectname}_add.value;
1327 var optionName = new Option(myvalue,myvalue,false,true);
1328 var length = object.$selectname.length;
1329 object.$selectname.options[length] = optionName;
1330 object.${selectname}_add.value = "";
1333 <SELECT MULTIPLE NAME="$selectname">
1336 foreach my $group ( @all_groups ) {
1338 if ( $sel_groups{$group} ) {
1339 $html .= ' SELECTED';
1340 $sel_groups{$group} = 0;
1342 $html .= ">$group</OPTION>\n";
1344 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1345 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1347 $html .= '</SELECT>';
1349 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1350 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1359 The $recref stuff in sub check should be cleaned up.
1361 The suspend, unsuspend and cancel methods update the database, but not the
1362 current object. This is probably a bug as it's unexpected and
1365 radius_usergroup_selector? putting web ui components in here? they should
1366 probably live somewhere else...
1368 insertion of RADIUS group stuff in insert could be done with child_objects now
1369 (would probably clean up export of them too)
1373 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1374 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1375 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1376 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1377 schema.html from the base documentation.