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
10 $welcome_template $welcome_from $welcome_subject $welcome_mimetype
17 use FS::UID qw( datasrc );
19 use FS::Record qw( qsearch qsearchs fields dbh );
24 use FS::cust_main_invoice;
28 use FS::radius_usergroup;
31 use FS::Msgcat qw(gettext);
33 @ISA = qw( FS::svc_Common );
36 $me = '[FS::svc_acct]';
38 #ask FS::UID to run this stuff for us later
39 $FS::UID::callback{'FS::svc_acct'} = sub {
41 $dir_prefix = $conf->config('home');
42 @shells = $conf->config('shells');
43 $usernamemin = $conf->config('usernamemin') || 2;
44 $usernamemax = $conf->config('usernamemax');
45 $passwordmin = $conf->config('passwordmin') || 6;
46 $passwordmax = $conf->config('passwordmax') || 8;
47 $username_letter = $conf->exists('username-letter');
48 $username_letterfirst = $conf->exists('username-letterfirst');
49 $username_noperiod = $conf->exists('username-noperiod');
50 $username_nounderscore = $conf->exists('username-nounderscore');
51 $username_nodash = $conf->exists('username-nodash');
52 $username_uppercase = $conf->exists('username-uppercase');
53 $username_ampersand = $conf->exists('username-ampersand');
54 $dirhash = $conf->config('dirhash') || 0;
55 if ( $conf->exists('welcome_email') ) {
56 $welcome_template = new Text::Template (
58 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
59 ) or warn "can't create welcome email template: $Text::Template::ERROR";
60 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
61 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
62 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
64 $welcome_template = '';
66 $smtpmachine = $conf->config('smtpmachine');
67 $radius_password = $conf->config('radius-password') || 'Password';
70 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
71 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
75 my ( $hashref, $cache ) = @_;
76 if ( $hashref->{'svc_acct_svcnum'} ) {
77 $self->{'_domsvc'} = FS::svc_domain->new( {
78 'svcnum' => $hashref->{'domsvc'},
79 'domain' => $hashref->{'svc_acct_domain'},
80 'catchall' => $hashref->{'svc_acct_catchall'},
87 FS::svc_acct - Object methods for svc_acct records
93 $record = new FS::svc_acct \%hash;
94 $record = new FS::svc_acct { 'column' => 'value' };
96 $error = $record->insert;
98 $error = $new_record->replace($old_record);
100 $error = $record->delete;
102 $error = $record->check;
104 $error = $record->suspend;
106 $error = $record->unsuspend;
108 $error = $record->cancel;
110 %hash = $record->radius;
112 %hash = $record->radius_reply;
114 %hash = $record->radius_check;
116 $domain = $record->domain;
118 $svc_domain = $record->svc_domain;
120 $email = $record->email;
122 $seconds_since = $record->seconds_since($timestamp);
126 An FS::svc_acct object represents an account. FS::svc_acct inherits from
127 FS::svc_Common. The following fields are currently supported:
131 =item svcnum - primary key (assigned automatcially for new accounts)
135 =item _password - generated if blank
137 =item sec_phrase - security phrase
139 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
147 =item dir - set automatically if blank (and uid is not)
151 =item quota - (unimplementd)
153 =item slipip - IP address
157 =item domsvc - svcnum from svc_domain
159 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
169 Creates a new account. To add the account to the database, see L<"insert">.
173 sub table { 'svc_acct'; }
177 Adds this account to the database. If there is an error, returns the error,
178 otherwise returns false.
180 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
181 defined. An FS::cust_svc record will be created and inserted.
183 The additional field I<usergroup> can optionally be defined; if so it should
184 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
185 sqlradius export only)
187 (TODOC: L<FS::queue> and L<freeside-queued>)
189 (TODOC: new exports! $noexport_hack)
197 local $SIG{HUP} = 'IGNORE';
198 local $SIG{INT} = 'IGNORE';
199 local $SIG{QUIT} = 'IGNORE';
200 local $SIG{TERM} = 'IGNORE';
201 local $SIG{TSTP} = 'IGNORE';
202 local $SIG{PIPE} = 'IGNORE';
204 my $oldAutoCommit = $FS::UID::AutoCommit;
205 local $FS::UID::AutoCommit = 0;
208 $error = $self->check;
209 return $error if $error;
211 #no, duplicate checking just got a whole lot more complicated
212 #(perhaps keep this check with a config option to turn on?)
214 #return gettext('username_in_use'). ": ". $self->username
215 # if qsearchs( 'svc_acct', { 'username' => $self->username,
216 # 'domsvc' => $self->domsvc,
219 if ( $self->svcnum ) {
220 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
221 unless ( $cust_svc ) {
222 $dbh->rollback if $oldAutoCommit;
223 return "no cust_svc record found for svcnum ". $self->svcnum;
225 $self->pkgnum($cust_svc->pkgnum);
226 $self->svcpart($cust_svc->svcpart);
229 #new duplicate username checking
231 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
232 unless ( $part_svc ) {
233 $dbh->rollback if $oldAutoCommit;
234 return 'unknown svcpart '. $self->svcpart;
237 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
238 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
239 'domsvc' => $self->domsvc } );
241 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
242 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
243 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
248 if ( @dup_user || @dup_userdomain || @dup_uid ) {
249 my $exports = FS::part_export::export_info('svc_acct');
250 my %conflict_user_svcpart;
251 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
253 foreach my $part_export ( $part_svc->part_export ) {
255 #this will catch to the same exact export
256 my @svcparts = map { $_->svcpart }
257 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
259 #this will catch to exports w/same exporthost+type ???
260 #my @other_part_export = qsearch('part_export', {
261 # 'machine' => $part_export->machine,
262 # 'exporttype' => $part_export->exporttype,
264 #foreach my $other_part_export ( @other_part_export ) {
265 # push @svcparts, map { $_->svcpart }
266 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
269 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
270 #silly kludge to avoid uninitialized value errors
271 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
272 ? $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 return "Can't delete an account which is a (svc_forward) source!"
422 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
424 return "Can't delete an account which is a (svc_forward) destination!"
425 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
427 return "Can't delete an account with (svc_www) web service!"
428 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
430 # what about records in session ? (they should refer to history table)
432 local $SIG{HUP} = 'IGNORE';
433 local $SIG{INT} = 'IGNORE';
434 local $SIG{QUIT} = 'IGNORE';
435 local $SIG{TERM} = 'IGNORE';
436 local $SIG{TSTP} = 'IGNORE';
437 local $SIG{PIPE} = 'IGNORE';
439 my $oldAutoCommit = $FS::UID::AutoCommit;
440 local $FS::UID::AutoCommit = 0;
443 foreach my $cust_main_invoice (
444 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
446 unless ( defined($cust_main_invoice) ) {
447 warn "WARNING: something's wrong with qsearch";
450 my %hash = $cust_main_invoice->hash;
451 $hash{'dest'} = $self->email;
452 my $new = new FS::cust_main_invoice \%hash;
453 my $error = $new->replace($cust_main_invoice);
455 $dbh->rollback if $oldAutoCommit;
460 foreach my $svc_domain (
461 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
463 my %hash = new FS::svc_domain->hash;
464 $hash{'catchall'} = '';
465 my $new = new FS::svc_domain \%hash;
466 my $error = $new->replace($svc_domain);
468 $dbh->rollback if $oldAutoCommit;
473 foreach my $radius_usergroup (
474 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
476 my $error = $radius_usergroup->delete;
478 $dbh->rollback if $oldAutoCommit;
483 my $error = $self->SUPER::delete;
485 $dbh->rollback if $oldAutoCommit;
489 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
493 =item replace OLD_RECORD
495 Replaces OLD_RECORD with this one in the database. If there is an error,
496 returns the error, otherwise returns false.
498 The additional field I<usergroup> can optionally be defined; if so it should
499 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
500 sqlradius export only)
505 my ( $new, $old ) = ( shift, shift );
507 warn "$me replacing $old with $new\n" if $DEBUG;
509 return "Username in use"
510 if $old->username ne $new->username &&
511 qsearchs( 'svc_acct', { 'username' => $new->username,
512 'domsvc' => $new->domsvc,
515 #no warnings 'numeric'; #alas, a 5.006-ism
517 return "Can't change uid!" if $old->uid != $new->uid;
520 #change homdir when we change username
521 $new->setfield('dir', '') if $old->username ne $new->username;
523 local $SIG{HUP} = 'IGNORE';
524 local $SIG{INT} = 'IGNORE';
525 local $SIG{QUIT} = 'IGNORE';
526 local $SIG{TERM} = 'IGNORE';
527 local $SIG{TSTP} = 'IGNORE';
528 local $SIG{PIPE} = 'IGNORE';
530 my $oldAutoCommit = $FS::UID::AutoCommit;
531 local $FS::UID::AutoCommit = 0;
534 # redundant, but so $new->usergroup gets set
535 $error = $new->check;
536 return $error if $error;
538 $old->usergroup( [ $old->radius_groups ] );
539 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
540 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
541 if ( $new->usergroup ) {
542 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
543 my @newgroups = @{$new->usergroup};
544 foreach my $oldgroup ( @{$old->usergroup} ) {
545 if ( grep { $oldgroup eq $_ } @newgroups ) {
546 @newgroups = grep { $oldgroup ne $_ } @newgroups;
549 my $radius_usergroup = qsearchs('radius_usergroup', {
550 svcnum => $old->svcnum,
551 groupname => $oldgroup,
553 my $error = $radius_usergroup->delete;
555 $dbh->rollback if $oldAutoCommit;
556 return "error deleting radius_usergroup $oldgroup: $error";
560 foreach my $newgroup ( @newgroups ) {
561 my $radius_usergroup = new FS::radius_usergroup ( {
562 svcnum => $new->svcnum,
563 groupname => $newgroup,
565 my $error = $radius_usergroup->insert;
567 $dbh->rollback if $oldAutoCommit;
568 return "error adding radius_usergroup $newgroup: $error";
574 $error = $new->SUPER::replace($old);
576 $dbh->rollback if $oldAutoCommit;
577 return $error if $error;
580 if ( $new->username ne $old->username ) {
581 #false laziness with sub insert (and cust_main)
582 my $queue = new FS::queue {
583 'svcnum' => $new->svcnum,
584 'job' => 'FS::svc_acct::append_fuzzyfiles'
586 $error = $queue->insert($new->username);
588 $dbh->rollback if $oldAutoCommit;
589 return "queueing job (transaction rolled back): $error";
593 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
599 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
600 error, returns the error, otherwise returns false.
602 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
608 my %hash = $self->hash;
609 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
610 || $hash{_password} eq '*'
612 $hash{_password} = '*SUSPENDED* '.$hash{_password};
613 my $new = new FS::svc_acct ( \%hash );
614 my $error = $new->replace($self);
615 return $error if $error;
618 $self->SUPER::suspend;
623 Unsuspends this account by removing *SUSPENDED* from the password. If there is
624 an error, returns the error, otherwise returns false.
626 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
632 my %hash = $self->hash;
633 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
634 $hash{_password} = $1;
635 my $new = new FS::svc_acct ( \%hash );
636 my $error = $new->replace($self);
637 return $error if $error;
640 $self->SUPER::unsuspend;
645 Just returns false (no error) for now.
647 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
651 Checks all fields to make sure this is a valid service. If there is an error,
652 returns the error, otherwise returns false. Called by the insert and replace
655 Sets any fixed values; see L<FS::part_svc>.
662 my($recref) = $self->hashref;
664 my $x = $self->setfixed;
665 return $x unless ref($x);
668 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
670 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
673 my $error = $self->ut_numbern('svcnum')
674 || $self->ut_number('domsvc')
675 || $self->ut_textn('sec_phrase')
677 return $error if $error;
679 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
680 if ( $username_uppercase ) {
681 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
682 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
683 $recref->{username} = $1;
685 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
686 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
687 $recref->{username} = $1;
690 if ( $username_letterfirst ) {
691 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
692 } elsif ( $username_letter ) {
693 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
695 if ( $username_noperiod ) {
696 $recref->{username} =~ /\./ and return gettext('illegal_username');
698 if ( $username_nounderscore ) {
699 $recref->{username} =~ /_/ and return gettext('illegal_username');
701 if ( $username_nodash ) {
702 $recref->{username} =~ /\-/ and return gettext('illegal_username');
704 unless ( $username_ampersand ) {
705 $recref->{username} =~ /\&/ and return gettext('illegal_username');
708 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
709 $recref->{popnum} = $1;
710 return "Unknown popnum" unless
711 ! $recref->{popnum} ||
712 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
714 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
716 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
717 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
719 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
720 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
721 #not all systems use gid=uid
722 #you can set a fixed gid in part_svc
724 return "Only root can have uid 0"
725 if $recref->{uid} == 0
726 && $recref->{username} ne 'root'
727 && $recref->{username} ne 'toor';
730 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
731 or return "Illegal directory: ". $recref->{dir};
733 return "Illegal directory"
734 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
735 return "Illegal directory"
736 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
737 unless ( $recref->{dir} ) {
738 $recref->{dir} = $dir_prefix . '/';
739 if ( $dirhash > 0 ) {
740 for my $h ( 1 .. $dirhash ) {
741 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
743 } elsif ( $dirhash < 0 ) {
744 for my $h ( reverse $dirhash .. -1 ) {
745 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
748 $recref->{dir} .= $recref->{username};
752 unless ( $recref->{username} eq 'sync' ) {
753 if ( grep $_ eq $recref->{shell}, @shells ) {
754 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
756 return "Illegal shell \`". $self->shell. "\'; ".
757 $conf->dir. "/shells contains: @shells";
760 $recref->{shell} = '/bin/sync';
764 $recref->{gid} ne '' ?
765 return "Can't have gid without uid" : ( $recref->{gid}='' );
766 $recref->{dir} ne '' ?
767 return "Can't have directory without uid" : ( $recref->{dir}='' );
768 $recref->{shell} ne '' ?
769 return "Can't have shell without uid" : ( $recref->{shell}='' );
772 # $error = $self->ut_textn('finger');
773 # return $error if $error;
774 $self->getfield('finger') =~
775 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
776 or return "Illegal finger: ". $self->getfield('finger');
777 $self->setfield('finger', $1);
779 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
780 $recref->{quota} = $1;
782 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
783 unless ( $recref->{slipip} eq '0e0' ) {
784 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
785 or return "Illegal slipip: ". $self->slipip;
786 $recref->{slipip} = $1;
788 $recref->{slipip} = '0e0';
793 #arbitrary RADIUS stuff; allow ut_textn for now
794 foreach ( grep /^radius_/, fields('svc_acct') ) {
798 #generate a password if it is blank
799 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
800 unless ( $recref->{_password} );
802 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
803 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
804 $recref->{_password} = $1.$3;
805 #uncomment this to encrypt password immediately upon entry, or run
806 #bin/crypt_pw in cron to give new users a window during which their
807 #password is available to techs, for faxing, etc. (also be aware of
809 #$recref->{password} = $1.
810 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
812 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
813 $recref->{_password} = $1.$3;
814 } elsif ( $recref->{_password} eq '*' ) {
815 $recref->{_password} = '*';
816 } elsif ( $recref->{_password} eq '!!' ) {
817 $recref->{_password} = '!!';
819 #return "Illegal password";
820 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
821 FS::Msgcat::_gettext('illegal_password_characters').
822 ": ". $recref->{_password};
830 Depriciated, use radius_reply instead.
835 carp "FS::svc_acct::radius depriciated, use radius_reply";
841 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
842 reply attributes of this record.
844 Note that this is now the preferred method for reading RADIUS attributes -
845 accessing the columns directly is discouraged, as the column names are
846 expected to change in the future.
855 my($column, $attrib) = ($1, $2);
856 #$attrib =~ s/_/\-/g;
857 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
858 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
859 if ( $self->slipip && $self->slipip ne '0e0' ) {
860 $reply{'Framed-IP-Address'} = $self->slipip;
867 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
868 check attributes of this record.
870 Note that this is now the preferred method for reading RADIUS attributes -
871 accessing the columns directly is discouraged, as the column names are
872 expected to change in the future.
878 my $password = $self->_password;
879 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
880 ( $pw_attrib => $password,
883 my($column, $attrib) = ($1, $2);
884 #$attrib =~ s/_/\-/g;
885 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
886 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
892 Returns the domain associated with this account.
898 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
899 my $svc_domain = $self->svc_domain
900 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
906 Returns the FS::svc_domain record for this account's domain (see
915 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
920 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
924 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
929 Returns an email address associated with the account.
935 $self->username. '@'. $self->domain;
938 =item seconds_since TIMESTAMP
940 Returns the number of seconds this account has been online since TIMESTAMP,
941 according to the session monitor (see L<FS::Session>).
943 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
944 L<Time::Local> and L<Date::Parse> for conversion functions.
948 #note: POD here, implementation in FS::cust_svc
951 $self->cust_svc->seconds_since(@_);
954 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
956 Returns the numbers of seconds this account has been online between
957 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
958 external SQL radacct table, specified via sqlradius export. Sessions which
959 started in the specified range but are still open are counted from session
960 start to the end of the range (unless they are over 1 day old, in which case
961 they are presumed missing their stop record and not counted). Also, sessions
962 which end in therange but started earlier are counted from the start of the
963 range to session end. Finally, sessions which start before the range but end
964 after are counted for the entire range.
966 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
967 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
972 #note: POD here, implementation in FS::cust_svc
973 sub seconds_since_sqlradacct {
975 $self->cust_svc->seconds_since_sqlradacct(@_);
978 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
980 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
981 in this package for sessions ending between TIMESTAMP_START (inclusive) and
982 TIMESTAMP_END (exclusive).
984 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
985 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
990 #note: POD here, implementation in FS::cust_svc
991 sub attribute_since_sqlradacct {
993 $self->cust_svc->attribute_since_sqlradacct(@_);
998 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1004 if ( $self->usergroup ) {
1005 #when provisioning records, export callback runs in svc_Common.pm before
1006 #radius_usergroup records can be inserted...
1007 @{$self->usergroup};
1009 map { $_->groupname }
1010 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1028 use Mail::Internet 1.44;
1031 $opt{mimetype} ||= 'text/plain';
1032 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1034 $ENV{MAILADDRESS} = $opt{from};
1035 my $header = new Mail::Header ( [
1038 "Sender: $opt{from}",
1039 "Reply-To: $opt{from}",
1040 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1041 "Subject: $opt{subject}",
1042 "Content-Type: $opt{mimetype}",
1044 my $message = new Mail::Internet (
1045 'Header' => $header,
1046 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1049 $message->smtpsend( Host => $smtpmachine )
1050 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1051 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1054 =item check_and_rebuild_fuzzyfiles
1058 sub check_and_rebuild_fuzzyfiles {
1059 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1060 -e "$dir/svc_acct.username"
1061 or &rebuild_fuzzyfiles;
1064 =item rebuild_fuzzyfiles
1068 sub rebuild_fuzzyfiles {
1070 use Fcntl qw(:flock);
1072 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1076 open(USERNAMELOCK,">>$dir/svc_acct.username")
1077 or die "can't open $dir/svc_acct.username: $!";
1078 flock(USERNAMELOCK,LOCK_EX)
1079 or die "can't lock $dir/svc_acct.username: $!";
1081 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1083 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1084 or die "can't open $dir/svc_acct.username.tmp: $!";
1085 print USERNAMECACHE join("\n", @all_username), "\n";
1086 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1088 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1098 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1099 open(USERNAMECACHE,"<$dir/svc_acct.username")
1100 or die "can't open $dir/svc_acct.username: $!";
1101 my @array = map { chomp; $_; } <USERNAMECACHE>;
1102 close USERNAMECACHE;
1106 =item append_fuzzyfiles USERNAME
1110 sub append_fuzzyfiles {
1111 my $username = shift;
1113 &check_and_rebuild_fuzzyfiles;
1115 use Fcntl qw(:flock);
1117 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1119 open(USERNAME,">>$dir/svc_acct.username")
1120 or die "can't open $dir/svc_acct.username: $!";
1121 flock(USERNAME,LOCK_EX)
1122 or die "can't lock $dir/svc_acct.username: $!";
1124 print USERNAME "$username\n";
1126 flock(USERNAME,LOCK_UN)
1127 or die "can't unlock $dir/svc_acct.username: $!";
1135 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1139 sub radius_usergroup_selector {
1140 my $sel_groups = shift;
1141 my %sel_groups = map { $_=>1 } @$sel_groups;
1143 my $selectname = shift || 'radius_usergroup';
1146 my $sth = $dbh->prepare(
1147 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1148 ) or die $dbh->errstr;
1149 $sth->execute() or die $sth->errstr;
1150 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1154 function ${selectname}_doadd(object) {
1155 var myvalue = object.${selectname}_add.value;
1156 var optionName = new Option(myvalue,myvalue,false,true);
1157 var length = object.$selectname.length;
1158 object.$selectname.options[length] = optionName;
1159 object.${selectname}_add.value = "";
1162 <SELECT MULTIPLE NAME="$selectname">
1165 foreach my $group ( @all_groups ) {
1167 if ( $sel_groups{$group} ) {
1168 $html .= ' SELECTED';
1169 $sel_groups{$group} = 0;
1171 $html .= ">$group</OPTION>\n";
1173 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1174 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1176 $html .= '</SELECT>';
1178 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1179 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1188 The $recref stuff in sub check should be cleaned up.
1190 The suspend, unsuspend and cancel methods update the database, but not the
1191 current object. This is probably a bug as it's unexpected and
1194 radius_usergroup_selector? putting web ui components in here? they should
1195 probably live somewhere else...
1199 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1200 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1201 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1202 L<freeside-queued>), L<FS::svc_acct_pop>,
1203 schema.html from the base documentation.