4 use vars qw( @ISA $DEBUG $me $noexport_hack $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
18 use FS::UID qw( datasrc );
20 use FS::Record qw( qsearch qsearchs fields dbh );
27 use FS::cust_main_invoice;
31 use FS::radius_usergroup;
34 use FS::Msgcat qw(gettext);
36 @ISA = qw( FS::svc_Common );
39 $me = '[FS::svc_acct]';
41 #ask FS::UID to run this stuff for us later
42 $FS::UID::callback{'FS::svc_acct'} = sub {
44 $dir_prefix = $conf->config('home');
45 @shells = $conf->config('shells');
46 $usernamemin = $conf->config('usernamemin') || 2;
47 $usernamemax = $conf->config('usernamemax');
48 $passwordmin = $conf->config('passwordmin') || 6;
49 $passwordmax = $conf->config('passwordmax') || 8;
50 $username_letter = $conf->exists('username-letter');
51 $username_letterfirst = $conf->exists('username-letterfirst');
52 $username_noperiod = $conf->exists('username-noperiod');
53 $username_nounderscore = $conf->exists('username-nounderscore');
54 $username_nodash = $conf->exists('username-nodash');
55 $username_uppercase = $conf->exists('username-uppercase');
56 $username_ampersand = $conf->exists('username-ampersand');
57 $mydomain = $conf->config('domain');
58 $dirhash = $conf->config('dirhash') || 0;
59 if ( $conf->exists('welcome_email') ) {
60 $welcome_template = new Text::Template (
62 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
63 ) or warn "can't create welcome email template: $Text::Template::ERROR";
64 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
65 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
66 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
68 $welcome_template = '';
70 $smtpmachine = $conf->config('smtpmachine');
71 $radius_password = $conf->config('radius-password') || 'Password';
74 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
75 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
79 my ( $hashref, $cache ) = @_;
80 if ( $hashref->{'svc_acct_svcnum'} ) {
81 $self->{'_domsvc'} = FS::svc_domain->new( {
82 'svcnum' => $hashref->{'domsvc'},
83 'domain' => $hashref->{'svc_acct_domain'},
84 'catchall' => $hashref->{'svc_acct_catchall'},
91 FS::svc_acct - Object methods for svc_acct records
97 $record = new FS::svc_acct \%hash;
98 $record = new FS::svc_acct { 'column' => 'value' };
100 $error = $record->insert;
102 $error = $new_record->replace($old_record);
104 $error = $record->delete;
106 $error = $record->check;
108 $error = $record->suspend;
110 $error = $record->unsuspend;
112 $error = $record->cancel;
114 %hash = $record->radius;
116 %hash = $record->radius_reply;
118 %hash = $record->radius_check;
120 $domain = $record->domain;
122 $svc_domain = $record->svc_domain;
124 $email = $record->email;
126 $seconds_since = $record->seconds_since($timestamp);
130 An FS::svc_acct object represents an account. FS::svc_acct inherits from
131 FS::svc_Common. The following fields are currently supported:
135 =item svcnum - primary key (assigned automatcially for new accounts)
139 =item _password - generated if blank
141 =item sec_phrase - security phrase
143 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
151 =item dir - set automatically if blank (and uid is not)
155 =item quota - (unimplementd)
157 =item slipip - IP address
161 =item domsvc - svcnum from svc_domain
163 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
173 Creates a new account. To add the account to the database, see L<"insert">.
177 sub table { 'svc_acct'; }
181 Adds this account to the database. If there is an error, returns the error,
182 otherwise returns false.
184 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
185 defined. An FS::cust_svc record will be created and inserted.
187 The additional field I<usergroup> can optionally be defined; if so it should
188 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
189 sqlradius export only)
191 (TODOC: L<FS::queue> and L<freeside-queued>)
193 (TODOC: new exports! $noexport_hack)
201 local $SIG{HUP} = 'IGNORE';
202 local $SIG{INT} = 'IGNORE';
203 local $SIG{QUIT} = 'IGNORE';
204 local $SIG{TERM} = 'IGNORE';
205 local $SIG{TSTP} = 'IGNORE';
206 local $SIG{PIPE} = 'IGNORE';
208 my $oldAutoCommit = $FS::UID::AutoCommit;
209 local $FS::UID::AutoCommit = 0;
212 $error = $self->check;
213 return $error if $error;
215 #no, duplicate checking just got a whole lot more complicated
216 #(perhaps keep this check with a config option to turn on?)
218 #return gettext('username_in_use'). ": ". $self->username
219 # if qsearchs( 'svc_acct', { 'username' => $self->username,
220 # 'domsvc' => $self->domsvc,
223 if ( $self->svcnum ) {
224 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
225 unless ( $cust_svc ) {
226 $dbh->rollback if $oldAutoCommit;
227 return "no cust_svc record found for svcnum ". $self->svcnum;
229 $self->pkgnum($cust_svc->pkgnum);
230 $self->svcpart($cust_svc->svcpart);
233 #new duplicate username checking
235 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
236 unless ( $part_svc ) {
237 $dbh->rollback if $oldAutoCommit;
238 return 'unknown svcpart '. $self->svcpart;
241 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
242 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
243 'domsvc' => $self->domsvc } );
245 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
246 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
247 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
252 if ( @dup_user || @dup_userdomain || @dup_uid ) {
253 my $exports = FS::part_export::export_info('svc_acct');
254 my %conflict_user_svcpart;
255 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
257 foreach my $part_export ( $part_svc->part_export ) {
259 #this will catch to the same exact export
260 my @svcparts = map { $_->svcpart }
261 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
263 #this will catch to exports w/same exporthost+type ???
264 #my @other_part_export = qsearch('part_export', {
265 # 'machine' => $part_export->machine,
266 # 'exporttype' => $part_export->exporttype,
268 #foreach my $other_part_export ( @other_part_export ) {
269 # push @svcparts, map { $_->svcpart }
270 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
273 my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
274 if ( $nodomain =~ /^Y/i ) {
275 $conflict_user_svcpart{$_} = $part_export->exportnum
278 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
283 foreach my $dup_user ( @dup_user ) {
284 my $dup_svcpart = $dup_user->cust_svc->svcpart;
285 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
286 $dbh->rollback if $oldAutoCommit;
287 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
288 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
292 foreach my $dup_userdomain ( @dup_userdomain ) {
293 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
294 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
295 $dbh->rollback if $oldAutoCommit;
296 return "duplicate username\@domain: conflicts with svcnum ".
297 $dup_userdomain->svcnum. " via exportnum ".
298 $conflict_userdomain_svcpart{$dup_svcpart};
302 foreach my $dup_uid ( @dup_uid ) {
303 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
304 if ( exists($conflict_user_svcpart{$dup_svcpart})
305 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
306 $dbh->rollback if $oldAutoCommit;
307 return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
308 "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
309 || $conflict_userdomain_svcpart{$dup_svcpart};
315 #see? i told you it was more complicated
318 $error = $self->SUPER::insert(\@jobnums);
320 $dbh->rollback if $oldAutoCommit;
324 if ( $self->usergroup ) {
325 foreach my $groupname ( @{$self->usergroup} ) {
326 my $radius_usergroup = new FS::radius_usergroup ( {
327 svcnum => $self->svcnum,
328 groupname => $groupname,
330 my $error = $radius_usergroup->insert;
332 $dbh->rollback if $oldAutoCommit;
338 #false laziness with sub replace (and cust_main)
339 my $queue = new FS::queue {
340 'svcnum' => $self->svcnum,
341 'job' => 'FS::svc_acct::append_fuzzyfiles'
343 $error = $queue->insert($self->username);
345 $dbh->rollback if $oldAutoCommit;
346 return "queueing job (transaction rolled back): $error";
349 my $cust_pkg = $self->cust_svc->cust_pkg;
352 my $cust_main = $cust_pkg->cust_main;
354 if ( $conf->exists('emailinvoiceauto') ) {
355 my @invoicing_list = $cust_main->invoicing_list;
356 push @invoicing_list, $self->email;
357 $cust_main->invoicing_list(\@invoicing_list);
362 if ( $welcome_template && $cust_pkg ) {
363 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
365 my $wqueue = new FS::queue {
366 'svcnum' => $self->svcnum,
367 'job' => 'FS::svc_acct::send_email'
369 warn "attempting to queue email to $to";
370 my $error = $wqueue->insert(
372 'from' => $welcome_from,
373 'subject' => $welcome_subject,
374 'mimetype' => $welcome_mimetype,
375 'body' => $welcome_template->fill_in( HASH => {
376 'custnum' => $self->custnum,
377 'username' => $self->username,
378 'password' => $self->_password,
379 'first' => $cust_main->first,
380 'last' => $cust_main->getfield('last'),
381 'pkg' => $cust_pkg->part_pkg->pkg,
385 $dbh->rollback if $oldAutoCommit;
386 return "queuing welcome email: $error";
389 foreach my $jobnum ( @jobnums ) {
390 my $error = $wqueue->depend_insert($jobnum);
392 $dbh->rollback if $oldAutoCommit;
393 return "queuing welcome email job dependancy: $error";
403 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
409 Deletes this account from the database. If there is an error, returns the
410 error, otherwise returns false.
412 The corresponding FS::cust_svc record will be deleted as well.
414 (TODOC: new exports! $noexport_hack)
421 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
422 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
423 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
426 return "Can't delete an account which is a (svc_forward) source!"
427 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
429 return "Can't delete an account which is a (svc_forward) destination!"
430 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
432 return "Can't delete an account with (svc_www) web service!"
433 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
435 # what about records in session ? (they should refer to history table)
437 local $SIG{HUP} = 'IGNORE';
438 local $SIG{INT} = 'IGNORE';
439 local $SIG{QUIT} = 'IGNORE';
440 local $SIG{TERM} = 'IGNORE';
441 local $SIG{TSTP} = 'IGNORE';
442 local $SIG{PIPE} = 'IGNORE';
444 my $oldAutoCommit = $FS::UID::AutoCommit;
445 local $FS::UID::AutoCommit = 0;
448 foreach my $cust_main_invoice (
449 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
451 unless ( defined($cust_main_invoice) ) {
452 warn "WARNING: something's wrong with qsearch";
455 my %hash = $cust_main_invoice->hash;
456 $hash{'dest'} = $self->email;
457 my $new = new FS::cust_main_invoice \%hash;
458 my $error = $new->replace($cust_main_invoice);
460 $dbh->rollback if $oldAutoCommit;
465 foreach my $svc_domain (
466 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
468 my %hash = new FS::svc_domain->hash;
469 $hash{'catchall'} = '';
470 my $new = new FS::svc_domain \%hash;
471 my $error = $new->replace($svc_domain);
473 $dbh->rollback if $oldAutoCommit;
478 foreach my $radius_usergroup (
479 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
481 my $error = $radius_usergroup->delete;
483 $dbh->rollback if $oldAutoCommit;
488 my $error = $self->SUPER::delete;
490 $dbh->rollback if $oldAutoCommit;
494 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
498 =item replace OLD_RECORD
500 Replaces OLD_RECORD with this one in the database. If there is an error,
501 returns the error, otherwise returns false.
503 The additional field I<usergroup> can optionally be defined; if so it should
504 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
505 sqlradius export only)
510 my ( $new, $old ) = ( shift, shift );
512 warn "$me replacing $old with $new\n" if $DEBUG;
514 return "Username in use"
515 if $old->username ne $new->username &&
516 qsearchs( 'svc_acct', { 'username' => $new->username,
517 'domsvc' => $new->domsvc,
520 #no warnings 'numeric'; #alas, a 5.006-ism
522 return "Can't change uid!" if $old->uid != $new->uid;
525 #change homdir when we change username
526 $new->setfield('dir', '') if $old->username ne $new->username;
528 local $SIG{HUP} = 'IGNORE';
529 local $SIG{INT} = 'IGNORE';
530 local $SIG{QUIT} = 'IGNORE';
531 local $SIG{TERM} = 'IGNORE';
532 local $SIG{TSTP} = 'IGNORE';
533 local $SIG{PIPE} = 'IGNORE';
535 my $oldAutoCommit = $FS::UID::AutoCommit;
536 local $FS::UID::AutoCommit = 0;
539 # redundant, but so $new->usergroup gets set
540 $error = $new->check;
541 return $error if $error;
543 $old->usergroup( [ $old->radius_groups ] );
544 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
545 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
546 if ( $new->usergroup ) {
547 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
548 my @newgroups = @{$new->usergroup};
549 foreach my $oldgroup ( @{$old->usergroup} ) {
550 if ( grep { $oldgroup eq $_ } @newgroups ) {
551 @newgroups = grep { $oldgroup ne $_ } @newgroups;
554 my $radius_usergroup = qsearchs('radius_usergroup', {
555 svcnum => $old->svcnum,
556 groupname => $oldgroup,
558 my $error = $radius_usergroup->delete;
560 $dbh->rollback if $oldAutoCommit;
561 return "error deleting radius_usergroup $oldgroup: $error";
565 foreach my $newgroup ( @newgroups ) {
566 my $radius_usergroup = new FS::radius_usergroup ( {
567 svcnum => $new->svcnum,
568 groupname => $newgroup,
570 my $error = $radius_usergroup->insert;
572 $dbh->rollback if $oldAutoCommit;
573 return "error adding radius_usergroup $newgroup: $error";
579 $error = $new->SUPER::replace($old);
581 $dbh->rollback if $oldAutoCommit;
582 return $error if $error;
585 if ( $new->username ne $old->username ) {
586 #false laziness with sub insert (and cust_main)
587 my $queue = new FS::queue {
588 'svcnum' => $new->svcnum,
589 'job' => 'FS::svc_acct::append_fuzzyfiles'
591 $error = $queue->insert($new->username);
593 $dbh->rollback if $oldAutoCommit;
594 return "queueing job (transaction rolled back): $error";
598 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
604 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
605 error, returns the error, otherwise returns false.
607 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
613 my %hash = $self->hash;
614 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
615 || $hash{_password} eq '*'
617 $hash{_password} = '*SUSPENDED* '.$hash{_password};
618 my $new = new FS::svc_acct ( \%hash );
619 my $error = $new->replace($self);
620 return $error if $error;
623 $self->SUPER::suspend;
628 Unsuspends this account by removing *SUSPENDED* from the password. If there is
629 an error, returns the error, otherwise returns false.
631 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
637 my %hash = $self->hash;
638 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
639 $hash{_password} = $1;
640 my $new = new FS::svc_acct ( \%hash );
641 my $error = $new->replace($self);
642 return $error if $error;
645 $self->SUPER::unsuspend;
650 Just returns false (no error) for now.
652 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
656 Checks all fields to make sure this is a valid service. If there is an error,
657 returns the error, otherwise returns false. Called by the insert and replace
660 Sets any fixed values; see L<FS::part_svc>.
667 my($recref) = $self->hashref;
669 my $x = $self->setfixed;
670 return $x unless ref($x);
673 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
675 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
678 my $error = $self->ut_numbern('svcnum')
679 || $self->ut_number('domsvc')
680 || $self->ut_textn('sec_phrase')
682 return $error if $error;
684 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
685 if ( $username_uppercase ) {
686 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
687 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
688 $recref->{username} = $1;
690 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
691 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
692 $recref->{username} = $1;
695 if ( $username_letterfirst ) {
696 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
697 } elsif ( $username_letter ) {
698 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
700 if ( $username_noperiod ) {
701 $recref->{username} =~ /\./ and return gettext('illegal_username');
703 if ( $username_nounderscore ) {
704 $recref->{username} =~ /_/ and return gettext('illegal_username');
706 if ( $username_nodash ) {
707 $recref->{username} =~ /\-/ and return gettext('illegal_username');
709 unless ( $username_ampersand ) {
710 $recref->{username} =~ /\&/ and return gettext('illegal_username');
713 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
714 $recref->{popnum} = $1;
715 return "Unknown popnum" unless
716 ! $recref->{popnum} ||
717 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
719 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
721 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
722 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
724 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
725 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
726 #not all systems use gid=uid
727 #you can set a fixed gid in part_svc
729 return "Only root can have uid 0"
730 if $recref->{uid} == 0
731 && $recref->{username} ne 'root'
732 && $recref->{username} ne 'toor';
735 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
736 or return "Illegal directory: ". $recref->{dir};
738 return "Illegal directory"
739 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
740 return "Illegal directory"
741 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
742 unless ( $recref->{dir} ) {
743 $recref->{dir} = $dir_prefix . '/';
744 if ( $dirhash > 0 ) {
745 for my $h ( 1 .. $dirhash ) {
746 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
748 } elsif ( $dirhash < 0 ) {
749 for my $h ( reverse $dirhash .. -1 ) {
750 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
753 $recref->{dir} .= $recref->{username};
757 unless ( $recref->{username} eq 'sync' ) {
758 if ( grep $_ eq $recref->{shell}, @shells ) {
759 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
761 return "Illegal shell \`". $self->shell. "\'; ".
762 $conf->dir. "/shells contains: @shells";
765 $recref->{shell} = '/bin/sync';
769 $recref->{gid} ne '' ?
770 return "Can't have gid without uid" : ( $recref->{gid}='' );
771 $recref->{dir} ne '' ?
772 return "Can't have directory without uid" : ( $recref->{dir}='' );
773 $recref->{shell} ne '' ?
774 return "Can't have shell without uid" : ( $recref->{shell}='' );
777 # $error = $self->ut_textn('finger');
778 # return $error if $error;
779 $self->getfield('finger') =~
780 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
781 or return "Illegal finger: ". $self->getfield('finger');
782 $self->setfield('finger', $1);
784 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
785 $recref->{quota} = $1;
787 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
788 unless ( $recref->{slipip} eq '0e0' ) {
789 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
790 or return "Illegal slipip". $self->slipip;
791 $recref->{slipip} = $1;
793 $recref->{slipip} = '0e0';
798 #arbitrary RADIUS stuff; allow ut_textn for now
799 foreach ( grep /^radius_/, fields('svc_acct') ) {
803 #generate a password if it is blank
804 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
805 unless ( $recref->{_password} );
807 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
808 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
809 $recref->{_password} = $1.$3;
810 #uncomment this to encrypt password immediately upon entry, or run
811 #bin/crypt_pw in cron to give new users a window during which their
812 #password is available to techs, for faxing, etc. (also be aware of
814 #$recref->{password} = $1.
815 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
817 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
818 $recref->{_password} = $1.$3;
819 } elsif ( $recref->{_password} eq '*' ) {
820 $recref->{_password} = '*';
821 } elsif ( $recref->{_password} eq '!!' ) {
822 $recref->{_password} = '!!';
824 #return "Illegal password";
825 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
826 FS::Msgcat::_gettext('illegal_password_characters').
827 ": ". $recref->{_password};
835 Depriciated, use radius_reply instead.
840 carp "FS::svc_acct::radius depriciated, use radius_reply";
846 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
847 reply attributes of this record.
849 Note that this is now the preferred method for reading RADIUS attributes -
850 accessing the columns directly is discouraged, as the column names are
851 expected to change in the future.
860 my($column, $attrib) = ($1, $2);
861 #$attrib =~ s/_/\-/g;
862 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
863 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
864 if ( $self->slipip && $self->slipip ne '0e0' ) {
865 $reply{'Framed-IP-Address'} = $self->slipip;
872 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
873 check attributes of this record.
875 Note that this is now the preferred method for reading RADIUS attributes -
876 accessing the columns directly is discouraged, as the column names are
877 expected to change in the future.
883 my $password = $self->_password;
884 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
885 ( $pw_attrib => $self->_password,
888 my($column, $attrib) = ($1, $2);
889 #$attrib =~ s/_/\-/g;
890 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
891 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
897 Returns the domain associated with this account.
903 if ( $self->domsvc ) {
904 #$self->svc_domain->domain;
905 my $svc_domain = $self->svc_domain
906 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
909 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
915 Returns the FS::svc_domain record for this account's domain (see
924 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
929 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
933 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
938 Returns an email address associated with the account.
944 $self->username. '@'. $self->domain;
947 =item seconds_since TIMESTAMP
949 Returns the number of seconds this account has been online since TIMESTAMP,
950 according to the session monitor (see L<FS::Session>).
952 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
953 L<Time::Local> and L<Date::Parse> for conversion functions.
957 #note: POD here, implementation in FS::cust_svc
960 $self->cust_svc->seconds_since(@_);
963 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
965 Returns the numbers of seconds this account has been online between
966 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
967 external SQL radacct table, specified via sqlradius export. Sessions which
968 started in the specified range but are still open are counted from session
969 start to the end of the range (unless they are over 1 day old, in which case
970 they are presumed missing their stop record and not counted). Also, sessions
971 which end in the range but started earlier are counted from the start of the
972 range to session end. Finally, sessions which start before the range but end
973 after are counted for the entire range.
975 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
976 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
981 #note: POD here, implementation in FS::cust_svc
982 sub seconds_since_sqlradacct {
984 $self->cust_svc->seconds_since_sqlradacct(@_);
987 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
989 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
990 in this package for sessions ending between TIMESTAMP_START (inclusive) and
991 TIMESTAMP_END (exclusive).
993 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
994 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
999 #note: POD here, implementation in FS::cust_svc
1000 sub attribute_since_sqlradacct {
1002 $self->cust_svc->attribute_since_sqlradacct(@_);
1008 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1014 if ( $self->usergroup ) {
1015 #when provisioning records, export callback runs in svc_Common.pm before
1016 #radius_usergroup records can be inserted...
1017 @{$self->usergroup};
1019 map { $_->groupname }
1020 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1038 use Mail::Internet 1.44;
1041 $opt{mimetype} ||= 'text/plain';
1042 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1044 $ENV{MAILADDRESS} = $opt{from};
1045 my $header = new Mail::Header ( [
1048 "Sender: $opt{from}",
1049 "Reply-To: $opt{from}",
1050 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1051 "Subject: $opt{subject}",
1052 "Content-Type: $opt{mimetype}",
1054 my $message = new Mail::Internet (
1055 'Header' => $header,
1056 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1059 $message->smtpsend( Host => $smtpmachine )
1060 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1061 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1064 =item check_and_rebuild_fuzzyfiles
1068 sub check_and_rebuild_fuzzyfiles {
1069 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1070 -e "$dir/svc_acct.username"
1071 or &rebuild_fuzzyfiles;
1074 =item rebuild_fuzzyfiles
1078 sub rebuild_fuzzyfiles {
1080 use Fcntl qw(:flock);
1082 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1086 open(USERNAMELOCK,">>$dir/svc_acct.username")
1087 or die "can't open $dir/svc_acct.username: $!";
1088 flock(USERNAMELOCK,LOCK_EX)
1089 or die "can't lock $dir/svc_acct.username: $!";
1091 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1093 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1094 or die "can't open $dir/svc_acct.username.tmp: $!";
1095 print USERNAMECACHE join("\n", @all_username), "\n";
1096 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1098 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1108 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1109 open(USERNAMECACHE,"<$dir/svc_acct.username")
1110 or die "can't open $dir/svc_acct.username: $!";
1111 my @array = map { chomp; $_; } <USERNAMECACHE>;
1112 close USERNAMECACHE;
1116 =item append_fuzzyfiles USERNAME
1120 sub append_fuzzyfiles {
1121 my $username = shift;
1123 &check_and_rebuild_fuzzyfiles;
1125 use Fcntl qw(:flock);
1127 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1129 open(USERNAME,">>$dir/svc_acct.username")
1130 or die "can't open $dir/svc_acct.username: $!";
1131 flock(USERNAME,LOCK_EX)
1132 or die "can't lock $dir/svc_acct.username: $!";
1134 print USERNAME "$username\n";
1136 flock(USERNAME,LOCK_UN)
1137 or die "can't unlock $dir/svc_acct.username: $!";
1145 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1149 sub radius_usergroup_selector {
1150 my $sel_groups = shift;
1151 my %sel_groups = map { $_=>1 } @$sel_groups;
1153 my $selectname = shift || 'radius_usergroup';
1156 my $sth = $dbh->prepare(
1157 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1158 ) or die $dbh->errstr;
1159 $sth->execute() or die $sth->errstr;
1160 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1164 function ${selectname}_doadd(object) {
1165 var myvalue = object.${selectname}_add.value;
1166 var optionName = new Option(myvalue,myvalue,false,true);
1167 var length = object.$selectname.length;
1168 object.$selectname.options[length] = optionName;
1169 object.${selectname}_add.value = "";
1172 <SELECT MULTIPLE NAME="$selectname">
1175 foreach my $group ( @all_groups ) {
1177 if ( $sel_groups{$group} ) {
1178 $html .= ' SELECTED';
1179 $sel_groups{$group} = 0;
1181 $html .= ">$group</OPTION>\n";
1183 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1184 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1186 $html .= '</SELECT>';
1188 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1189 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1198 The $recref stuff in sub check should be cleaned up.
1200 The suspend, unsuspend and cancel methods update the database, but not the
1201 current object. This is probably a bug as it's unexpected and
1204 radius_usergroup_selector? putting web ui components in here? they should
1205 probably live somewhere else...
1209 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1210 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1211 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1212 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1213 schema.html from the base documentation.