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 #this is Pg-specific. what to do for mysql etc?
255 # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
256 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
257 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
259 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
261 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
262 unless ( $part_svc ) {
263 $dbh->rollback if $oldAutoCommit;
264 return 'unknown svcpart '. $self->svcpart;
267 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
268 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
269 'domsvc' => $self->domsvc } );
271 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
272 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
273 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
278 if ( @dup_user || @dup_userdomain || @dup_uid ) {
279 my $exports = FS::part_export::export_info('svc_acct');
280 my %conflict_user_svcpart;
281 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
283 foreach my $part_export ( $part_svc->part_export ) {
285 #this will catch to the same exact export
286 my @svcparts = map { $_->svcpart } $part_export->export_svc;
288 #this will catch to exports w/same exporthost+type ???
289 #my @other_part_export = qsearch('part_export', {
290 # 'machine' => $part_export->machine,
291 # 'exporttype' => $part_export->exporttype,
293 #foreach my $other_part_export ( @other_part_export ) {
294 # push @svcparts, map { $_->svcpart }
295 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
298 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
299 #silly kludge to avoid uninitialized value errors
300 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
301 ? $exports->{$part_export->exporttype}{'nodomain'}
303 if ( $nodomain =~ /^Y/i ) {
304 $conflict_user_svcpart{$_} = $part_export->exportnum
307 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
312 foreach my $dup_user ( @dup_user ) {
313 my $dup_svcpart = $dup_user->cust_svc->svcpart;
314 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
315 $dbh->rollback if $oldAutoCommit;
316 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
317 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
321 foreach my $dup_userdomain ( @dup_userdomain ) {
322 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
323 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
324 $dbh->rollback if $oldAutoCommit;
325 return "duplicate username\@domain: conflicts with svcnum ".
326 $dup_userdomain->svcnum. " via exportnum ".
327 $conflict_userdomain_svcpart{$dup_svcpart};
331 foreach my $dup_uid ( @dup_uid ) {
332 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
333 if ( exists($conflict_user_svcpart{$dup_svcpart})
334 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
335 $dbh->rollback if $oldAutoCommit;
336 return "duplicate uid: conflicts with svcnum ". $dup_uid->svcnum.
337 " via exportnum ". $conflict_user_svcpart{$dup_svcpart}
338 || $conflict_userdomain_svcpart{$dup_svcpart};
344 #see? i told you it was more complicated
347 $error = $self->SUPER::insert(
348 'jobnums' => \@jobnums,
349 'child_objects' => $self->child_objects,
353 $dbh->rollback if $oldAutoCommit;
357 if ( $self->usergroup ) {
358 foreach my $groupname ( @{$self->usergroup} ) {
359 my $radius_usergroup = new FS::radius_usergroup ( {
360 svcnum => $self->svcnum,
361 groupname => $groupname,
363 my $error = $radius_usergroup->insert;
365 $dbh->rollback if $oldAutoCommit;
371 #false laziness with sub replace (and cust_main)
372 my $queue = new FS::queue {
373 'svcnum' => $self->svcnum,
374 'job' => 'FS::svc_acct::append_fuzzyfiles'
376 $error = $queue->insert($self->username);
378 $dbh->rollback if $oldAutoCommit;
379 return "queueing job (transaction rolled back): $error";
382 my $cust_pkg = $self->cust_svc->cust_pkg;
385 my $cust_main = $cust_pkg->cust_main;
387 if ( $conf->exists('emailinvoiceauto') ) {
388 my @invoicing_list = $cust_main->invoicing_list;
389 push @invoicing_list, $self->email;
390 $cust_main->invoicing_list(\@invoicing_list);
395 if ( $welcome_template && $cust_pkg ) {
396 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
398 my $wqueue = new FS::queue {
399 'svcnum' => $self->svcnum,
400 'job' => 'FS::svc_acct::send_email'
402 my $error = $wqueue->insert(
404 'from' => $welcome_from,
405 'subject' => $welcome_subject,
406 'mimetype' => $welcome_mimetype,
407 'body' => $welcome_template->fill_in( HASH => {
408 'custnum' => $self->custnum,
409 'username' => $self->username,
410 'password' => $self->_password,
411 'first' => $cust_main->first,
412 'last' => $cust_main->getfield('last'),
413 'pkg' => $cust_pkg->part_pkg->pkg,
417 $dbh->rollback if $oldAutoCommit;
418 return "error queuing welcome email: $error";
421 if ( $options{'depend_jobnum'} ) {
422 warn "$me depend_jobnum found; adding to welcome email dependancies"
424 if ( ref($options{'depend_jobnum'}) ) {
425 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
426 "to welcome email dependancies"
428 push @jobnums, @{ $options{'depend_jobnum'} };
430 warn "$me adding job $options{'depend_jobnum'} ".
431 "to welcome email dependancies"
433 push @jobnums, $options{'depend_jobnum'};
437 foreach my $jobnum ( @jobnums ) {
438 my $error = $wqueue->depend_insert($jobnum);
440 $dbh->rollback if $oldAutoCommit;
441 return "error queuing welcome email job dependancy: $error";
451 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
457 Deletes this account from the database. If there is an error, returns the
458 error, otherwise returns false.
460 The corresponding FS::cust_svc record will be deleted as well.
462 (TODOC: new exports!)
469 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
470 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
471 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
474 return "can't delete system account" if $self->_check_system;
476 return "Can't delete an account which is a (svc_forward) source!"
477 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
479 return "Can't delete an account which is a (svc_forward) destination!"
480 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
482 return "Can't delete an account with (svc_www) web service!"
483 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
485 # what about records in session ? (they should refer to history table)
487 local $SIG{HUP} = 'IGNORE';
488 local $SIG{INT} = 'IGNORE';
489 local $SIG{QUIT} = 'IGNORE';
490 local $SIG{TERM} = 'IGNORE';
491 local $SIG{TSTP} = 'IGNORE';
492 local $SIG{PIPE} = 'IGNORE';
494 my $oldAutoCommit = $FS::UID::AutoCommit;
495 local $FS::UID::AutoCommit = 0;
498 foreach my $cust_main_invoice (
499 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
501 unless ( defined($cust_main_invoice) ) {
502 warn "WARNING: something's wrong with qsearch";
505 my %hash = $cust_main_invoice->hash;
506 $hash{'dest'} = $self->email;
507 my $new = new FS::cust_main_invoice \%hash;
508 my $error = $new->replace($cust_main_invoice);
510 $dbh->rollback if $oldAutoCommit;
515 foreach my $svc_domain (
516 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
518 my %hash = new FS::svc_domain->hash;
519 $hash{'catchall'} = '';
520 my $new = new FS::svc_domain \%hash;
521 my $error = $new->replace($svc_domain);
523 $dbh->rollback if $oldAutoCommit;
528 foreach my $radius_usergroup (
529 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
531 my $error = $radius_usergroup->delete;
533 $dbh->rollback if $oldAutoCommit;
538 my $error = $self->SUPER::delete;
540 $dbh->rollback if $oldAutoCommit;
544 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
548 =item replace OLD_RECORD
550 Replaces OLD_RECORD with this one in the database. If there is an error,
551 returns the error, otherwise returns false.
553 The additional field I<usergroup> can optionally be defined; if so it should
554 contain an arrayref of group names. See L<FS::radius_usergroup>.
560 my ( $new, $old ) = ( shift, shift );
562 warn "$me replacing $old with $new\n" if $DEBUG;
564 return "can't modify system account" if $old->_check_system;
566 return "Username in use"
567 if $old->username ne $new->username &&
568 qsearchs( 'svc_acct', { 'username' => $new->username,
569 'domsvc' => $new->domsvc,
572 #no warnings 'numeric'; #alas, a 5.006-ism
574 return "Can't change uid!" if $old->uid != $new->uid;
577 #change homdir when we change username
578 $new->setfield('dir', '') if $old->username ne $new->username;
580 local $SIG{HUP} = 'IGNORE';
581 local $SIG{INT} = 'IGNORE';
582 local $SIG{QUIT} = 'IGNORE';
583 local $SIG{TERM} = 'IGNORE';
584 local $SIG{TSTP} = 'IGNORE';
585 local $SIG{PIPE} = 'IGNORE';
587 my $oldAutoCommit = $FS::UID::AutoCommit;
588 local $FS::UID::AutoCommit = 0;
591 # redundant, but so $new->usergroup gets set
592 $error = $new->check;
593 return $error if $error;
595 $old->usergroup( [ $old->radius_groups ] );
596 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
597 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
598 if ( $new->usergroup ) {
599 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
600 my @newgroups = @{$new->usergroup};
601 foreach my $oldgroup ( @{$old->usergroup} ) {
602 if ( grep { $oldgroup eq $_ } @newgroups ) {
603 @newgroups = grep { $oldgroup ne $_ } @newgroups;
606 my $radius_usergroup = qsearchs('radius_usergroup', {
607 svcnum => $old->svcnum,
608 groupname => $oldgroup,
610 my $error = $radius_usergroup->delete;
612 $dbh->rollback if $oldAutoCommit;
613 return "error deleting radius_usergroup $oldgroup: $error";
617 foreach my $newgroup ( @newgroups ) {
618 my $radius_usergroup = new FS::radius_usergroup ( {
619 svcnum => $new->svcnum,
620 groupname => $newgroup,
622 my $error = $radius_usergroup->insert;
624 $dbh->rollback if $oldAutoCommit;
625 return "error adding radius_usergroup $newgroup: $error";
631 $error = $new->SUPER::replace($old);
633 $dbh->rollback if $oldAutoCommit;
634 return $error if $error;
637 if ( $new->username ne $old->username ) {
638 #false laziness with sub insert (and cust_main)
639 my $queue = new FS::queue {
640 'svcnum' => $new->svcnum,
641 'job' => 'FS::svc_acct::append_fuzzyfiles'
643 $error = $queue->insert($new->username);
645 $dbh->rollback if $oldAutoCommit;
646 return "queueing job (transaction rolled back): $error";
650 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
656 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
657 error, returns the error, otherwise returns false.
659 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
661 Calls any export-specific suspend hooks.
667 return "can't suspend system account" if $self->_check_system;
668 $self->SUPER::suspend;
673 Unsuspends this account by removing *SUSPENDED* from the password. If there is
674 an error, returns the error, otherwise returns false.
676 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
678 Calls any export-specific unsuspend hooks.
684 my %hash = $self->hash;
685 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
686 $hash{_password} = $1;
687 my $new = new FS::svc_acct ( \%hash );
688 my $error = $new->replace($self);
689 return $error if $error;
692 $self->SUPER::unsuspend;
697 Just returns false (no error) for now.
699 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
703 Checks all fields to make sure this is a valid service. If there is an error,
704 returns the error, otherwise returns false. Called by the insert and replace
707 Sets any fixed values; see L<FS::part_svc>.
714 my($recref) = $self->hashref;
716 my $x = $self->setfixed;
717 return $x unless ref($x);
720 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
722 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
725 my $error = $self->ut_numbern('svcnum')
726 #|| $self->ut_number('domsvc')
727 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
728 || $self->ut_textn('sec_phrase')
730 return $error if $error;
732 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
733 if ( $username_uppercase ) {
734 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
735 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
736 $recref->{username} = $1;
738 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
739 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
740 $recref->{username} = $1;
743 if ( $username_letterfirst ) {
744 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
745 } elsif ( $username_letter ) {
746 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
748 if ( $username_noperiod ) {
749 $recref->{username} =~ /\./ and return gettext('illegal_username');
751 if ( $username_nounderscore ) {
752 $recref->{username} =~ /_/ and return gettext('illegal_username');
754 if ( $username_nodash ) {
755 $recref->{username} =~ /\-/ and return gettext('illegal_username');
757 unless ( $username_ampersand ) {
758 $recref->{username} =~ /\&/ and return gettext('illegal_username');
761 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
762 $recref->{popnum} = $1;
763 return "Unknown popnum" unless
764 ! $recref->{popnum} ||
765 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
767 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
769 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
770 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
772 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
773 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
774 #not all systems use gid=uid
775 #you can set a fixed gid in part_svc
777 return "Only root can have uid 0"
778 if $recref->{uid} == 0
779 && $recref->{username} ne 'root'
780 && $recref->{username} ne 'toor';
783 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
784 or return "Illegal directory: ". $recref->{dir};
786 return "Illegal directory"
787 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
788 return "Illegal directory"
789 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
790 unless ( $recref->{dir} ) {
791 $recref->{dir} = $dir_prefix . '/';
792 if ( $dirhash > 0 ) {
793 for my $h ( 1 .. $dirhash ) {
794 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
796 } elsif ( $dirhash < 0 ) {
797 for my $h ( reverse $dirhash .. -1 ) {
798 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
801 $recref->{dir} .= $recref->{username};
805 unless ( $recref->{username} eq 'sync' ) {
806 if ( grep $_ eq $recref->{shell}, @shells ) {
807 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
809 return "Illegal shell \`". $self->shell. "\'; ".
810 $conf->dir. "/shells contains: @shells";
813 $recref->{shell} = '/bin/sync';
817 $recref->{gid} ne '' ?
818 return "Can't have gid without uid" : ( $recref->{gid}='' );
819 $recref->{dir} ne '' ?
820 return "Can't have directory without uid" : ( $recref->{dir}='' );
821 $recref->{shell} ne '' ?
822 return "Can't have shell without uid" : ( $recref->{shell}='' );
825 # $error = $self->ut_textn('finger');
826 # return $error if $error;
827 if ( $self->getfield('finger') eq '' ) {
828 my $cust_pkg = $self->svcnum
829 ? $self->cust_svc->cust_pkg
830 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
832 my $cust_main = $cust_pkg->cust_main;
833 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
836 $self->getfield('finger') =~
837 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
838 or return "Illegal finger: ". $self->getfield('finger');
839 $self->setfield('finger', $1);
841 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
842 $recref->{quota} = $1;
844 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
845 if ( $recref->{slipip} eq '' ) {
846 $recref->{slipip} = '';
847 } elsif ( $recref->{slipip} eq '0e0' ) {
848 $recref->{slipip} = '0e0';
850 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
851 or return "Illegal slipip". $self->slipip;
852 $recref->{slipip} = $1;
857 #arbitrary RADIUS stuff; allow ut_textn for now
858 foreach ( grep /^radius_/, fields('svc_acct') ) {
862 #generate a password if it is blank
863 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
864 unless ( $recref->{_password} );
866 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
867 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
868 $recref->{_password} = $1.$3;
869 #uncomment this to encrypt password immediately upon entry, or run
870 #bin/crypt_pw in cron to give new users a window during which their
871 #password is available to techs, for faxing, etc. (also be aware of
873 #$recref->{password} = $1.
874 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
876 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
877 $recref->{_password} = $1.$3;
878 } elsif ( $recref->{_password} eq '*' ) {
879 $recref->{_password} = '*';
880 } elsif ( $recref->{_password} eq '!' ) {
881 $recref->{_password} = '!';
882 } elsif ( $recref->{_password} eq '!!' ) {
883 $recref->{_password} = '!!';
885 #return "Illegal password";
886 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
887 FS::Msgcat::_gettext('illegal_password_characters').
888 ": ". $recref->{_password};
900 scalar( grep { $self->username eq $_ || $self->email eq $_ }
901 $conf->config('system_usernames')
908 Depriciated, use radius_reply instead.
913 carp "FS::svc_acct::radius depriciated, use radius_reply";
919 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
920 reply attributes of this record.
922 Note that this is now the preferred method for reading RADIUS attributes -
923 accessing the columns directly is discouraged, as the column names are
924 expected to change in the future.
933 my($column, $attrib) = ($1, $2);
934 #$attrib =~ s/_/\-/g;
935 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
936 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
937 if ( $self->slipip && $self->slipip ne '0e0' ) {
938 $reply{$radius_ip} = $self->slipip;
945 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
946 check attributes of this record.
948 Note that this is now the preferred method for reading RADIUS attributes -
949 accessing the columns directly is discouraged, as the column names are
950 expected to change in the future.
956 my $password = $self->_password;
957 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
958 ( $pw_attrib => $self->_password,
961 my($column, $attrib) = ($1, $2);
962 #$attrib =~ s/_/\-/g;
963 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
964 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
970 Returns the domain associated with this account.
976 if ( $self->domsvc ) {
977 #$self->svc_domain->domain;
978 my $svc_domain = $self->svc_domain
979 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
982 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
988 Returns the FS::svc_domain record for this account's domain (see
997 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1002 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1008 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
1013 Returns an email address associated with the account.
1019 $self->username. '@'. $self->domain;
1024 Returns an array of FS::acct_snarf records associated with the account.
1025 If the acct_snarf table does not exist or there are no associated records,
1026 an empty list is returned
1032 return () unless dbdef->table('acct_snarf');
1033 eval "use FS::acct_snarf;";
1035 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1038 =item seconds_since TIMESTAMP
1040 Returns the number of seconds this account has been online since TIMESTAMP,
1041 according to the session monitor (see L<FS::Session>).
1043 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1044 L<Time::Local> and L<Date::Parse> for conversion functions.
1048 #note: POD here, implementation in FS::cust_svc
1051 $self->cust_svc->seconds_since(@_);
1054 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1056 Returns the numbers of seconds this account has been online between
1057 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1058 external SQL radacct table, specified via sqlradius export. Sessions which
1059 started in the specified range but are still open are counted from session
1060 start to the end of the range (unless they are over 1 day old, in which case
1061 they are presumed missing their stop record and not counted). Also, sessions
1062 which end in the range but started earlier are counted from the start of the
1063 range to session end. Finally, sessions which start before the range but end
1064 after are counted for the entire range.
1066 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1067 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1072 #note: POD here, implementation in FS::cust_svc
1073 sub seconds_since_sqlradacct {
1075 $self->cust_svc->seconds_since_sqlradacct(@_);
1078 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1080 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1081 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1082 TIMESTAMP_END (exclusive).
1084 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1085 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1090 #note: POD here, implementation in FS::cust_svc
1091 sub attribute_since_sqlradacct {
1093 $self->cust_svc->attribute_since_sqlradacct(@_);
1097 =item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
1099 Returns an array of hash references of this customers login history for the
1100 given time range. (document this better)
1104 sub get_session_history_sqlradacct {
1106 $self->cust_svc->get_session_history_sqlradacct(@_);
1111 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1117 if ( $self->usergroup ) {
1118 #when provisioning records, export callback runs in svc_Common.pm before
1119 #radius_usergroup records can be inserted...
1120 @{$self->usergroup};
1122 map { $_->groupname }
1123 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1127 =item clone_suspended
1129 Constructor used by FS::part_export::_export_suspend fallback. Document
1134 sub clone_suspended {
1136 my %hash = $self->hash;
1137 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1138 new FS::svc_acct \%hash;
1141 =item clone_kludge_unsuspend
1143 Constructor used by FS::part_export::_export_unsuspend fallback. Document
1148 sub clone_kludge_unsuspend {
1150 my %hash = $self->hash;
1151 $hash{_password} = '';
1152 new FS::svc_acct \%hash;
1155 =item check_password
1157 Checks the supplied password against the (possibly encrypted) password in the
1158 database. Returns true for a sucessful authentication, false for no match.
1160 Currently supported encryptions are: classic DES crypt() and MD5
1164 sub check_password {
1165 my($self, $check_password) = @_;
1167 #remove old-style SUSPENDED kludge, they should be allowed to login to
1168 #self-service and pay up
1169 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
1171 #eventually should check a "password-encoding" field
1172 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
1174 } elsif ( length($password) < 13 ) { #plaintext
1175 $check_password eq $password;
1176 } elsif ( length($password) == 13 ) { #traditional DES crypt
1177 crypt($check_password, $password) eq $password;
1178 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
1179 unix_md5_crypt($check_password, $password) eq $password;
1180 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
1181 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
1182 $self->svcnum. "\n";
1185 warn "Can't check password: Unrecognized encryption for svcnum ".
1186 $self->svcnum. "\n";
1206 use Mail::Internet 1.44;
1209 $opt{mimetype} ||= 'text/plain';
1210 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1212 $ENV{MAILADDRESS} = $opt{from};
1213 my $header = new Mail::Header ( [
1216 "Sender: $opt{from}",
1217 "Reply-To: $opt{from}",
1218 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1219 "Subject: $opt{subject}",
1220 "Content-Type: $opt{mimetype}",
1222 my $message = new Mail::Internet (
1223 'Header' => $header,
1224 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1227 $message->smtpsend( Host => $smtpmachine )
1228 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1229 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1232 =item check_and_rebuild_fuzzyfiles
1236 sub check_and_rebuild_fuzzyfiles {
1237 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1238 -e "$dir/svc_acct.username"
1239 or &rebuild_fuzzyfiles;
1242 =item rebuild_fuzzyfiles
1246 sub rebuild_fuzzyfiles {
1248 use Fcntl qw(:flock);
1250 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1254 open(USERNAMELOCK,">>$dir/svc_acct.username")
1255 or die "can't open $dir/svc_acct.username: $!";
1256 flock(USERNAMELOCK,LOCK_EX)
1257 or die "can't lock $dir/svc_acct.username: $!";
1259 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1261 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1262 or die "can't open $dir/svc_acct.username.tmp: $!";
1263 print USERNAMECACHE join("\n", @all_username), "\n";
1264 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1266 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1276 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1277 open(USERNAMECACHE,"<$dir/svc_acct.username")
1278 or die "can't open $dir/svc_acct.username: $!";
1279 my @array = map { chomp; $_; } <USERNAMECACHE>;
1280 close USERNAMECACHE;
1284 =item append_fuzzyfiles USERNAME
1288 sub append_fuzzyfiles {
1289 my $username = shift;
1291 &check_and_rebuild_fuzzyfiles;
1293 use Fcntl qw(:flock);
1295 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1297 open(USERNAME,">>$dir/svc_acct.username")
1298 or die "can't open $dir/svc_acct.username: $!";
1299 flock(USERNAME,LOCK_EX)
1300 or die "can't lock $dir/svc_acct.username: $!";
1302 print USERNAME "$username\n";
1304 flock(USERNAME,LOCK_UN)
1305 or die "can't unlock $dir/svc_acct.username: $!";
1313 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1317 sub radius_usergroup_selector {
1318 my $sel_groups = shift;
1319 my %sel_groups = map { $_=>1 } @$sel_groups;
1321 my $selectname = shift || 'radius_usergroup';
1324 my $sth = $dbh->prepare(
1325 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1326 ) or die $dbh->errstr;
1327 $sth->execute() or die $sth->errstr;
1328 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1332 function ${selectname}_doadd(object) {
1333 var myvalue = object.${selectname}_add.value;
1334 var optionName = new Option(myvalue,myvalue,false,true);
1335 var length = object.$selectname.length;
1336 object.$selectname.options[length] = optionName;
1337 object.${selectname}_add.value = "";
1340 <SELECT MULTIPLE NAME="$selectname">
1343 foreach my $group ( @all_groups ) {
1345 if ( $sel_groups{$group} ) {
1346 $html .= ' SELECTED';
1347 $sel_groups{$group} = 0;
1349 $html .= ">$group</OPTION>\n";
1351 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1352 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1354 $html .= '</SELECT>';
1356 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1357 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1366 The $recref stuff in sub check should be cleaned up.
1368 The suspend, unsuspend and cancel methods update the database, but not the
1369 current object. This is probably a bug as it's unexpected and
1372 radius_usergroup_selector? putting web ui components in here? they should
1373 probably live somewhere else...
1375 insertion of RADIUS group stuff in insert could be done with child_objects now
1376 (would probably clean up export of them too)
1380 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1381 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1382 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1383 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1384 schema.html from the base documentation.