4 use vars qw( @ISA $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 );
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 $mydomain = $conf->config('domain');
55 $dirhash = $conf->config('dirhash') || 0;
56 if ( $conf->exists('welcome_email') ) {
57 $welcome_template = new Text::Template (
59 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
60 ) or warn "can't create welcome email template: $Text::Template::ERROR";
61 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
62 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
63 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
65 $welcome_template = '';
67 $smtpmachine = $conf->config('smtpmachine');
68 $radius_password = $conf->config('radius-password') || 'Password';
71 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
72 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
76 my ( $hashref, $cache ) = @_;
77 if ( $hashref->{'svc_acct_svcnum'} ) {
78 $self->{'_domsvc'} = FS::svc_domain->new( {
79 'svcnum' => $hashref->{'domsvc'},
80 'domain' => $hashref->{'svc_acct_domain'},
81 'catchall' => $hashref->{'svc_acct_catchall'},
88 FS::svc_acct - Object methods for svc_acct records
94 $record = new FS::svc_acct \%hash;
95 $record = new FS::svc_acct { 'column' => 'value' };
97 $error = $record->insert;
99 $error = $new_record->replace($old_record);
101 $error = $record->delete;
103 $error = $record->check;
105 $error = $record->suspend;
107 $error = $record->unsuspend;
109 $error = $record->cancel;
111 %hash = $record->radius;
113 %hash = $record->radius_reply;
115 %hash = $record->radius_check;
117 $domain = $record->domain;
119 $svc_domain = $record->svc_domain;
121 $email = $record->email;
123 $seconds_since = $record->seconds_since($timestamp);
127 An FS::svc_acct object represents an account. FS::svc_acct inherits from
128 FS::svc_Common. The following fields are currently supported:
132 =item svcnum - primary key (assigned automatcially for new accounts)
136 =item _password - generated if blank
138 =item sec_phrase - security phrase
140 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
148 =item dir - set automatically if blank (and uid is not)
152 =item quota - (unimplementd)
154 =item slipip - IP address
158 =item domsvc - svcnum from svc_domain
160 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
170 Creates a new account. To add the account to the database, see L<"insert">.
174 sub table { 'svc_acct'; }
178 Adds this account to the database. If there is an error, returns the error,
179 otherwise returns false.
181 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
182 defined. An FS::cust_svc record will be created and inserted.
184 The additional field I<usergroup> can optionally be defined; if so it should
185 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
186 sqlradius export only)
188 (TODOC: L<FS::queue> and L<freeside-queued>)
190 (TODOC: new exports! $noexport_hack)
198 local $SIG{HUP} = 'IGNORE';
199 local $SIG{INT} = 'IGNORE';
200 local $SIG{QUIT} = 'IGNORE';
201 local $SIG{TERM} = 'IGNORE';
202 local $SIG{TSTP} = 'IGNORE';
203 local $SIG{PIPE} = 'IGNORE';
205 my $oldAutoCommit = $FS::UID::AutoCommit;
206 local $FS::UID::AutoCommit = 0;
209 $error = $self->check;
210 return $error if $error;
212 #no, duplicate checking just got a whole lot more complicated
213 #(perhaps keep this check with a config option to turn on?)
215 #return gettext('username_in_use'). ": ". $self->username
216 # if qsearchs( 'svc_acct', { 'username' => $self->username,
217 # 'domsvc' => $self->domsvc,
220 if ( $self->svcnum ) {
221 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
222 unless ( $cust_svc ) {
223 $dbh->rollback if $oldAutoCommit;
224 return "no cust_svc record found for svcnum ". $self->svcnum;
226 $self->pkgnum($cust_svc->pkgnum);
227 $self->svcpart($cust_svc->svcpart);
230 #new duplicate username checking
232 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
233 unless ( $part_svc ) {
234 $dbh->rollback if $oldAutoCommit;
235 return 'unknown svcpart '. $self->svcpart;
238 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
239 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
240 'domsvc' => $self->domsvc } );
242 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
243 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
244 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
249 if ( @dup_user || @dup_userdomain || @dup_uid ) {
250 my $exports = FS::part_export::export_info('svc_acct');
251 my %conflict_user_svcpart;
252 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
254 foreach my $part_export ( $part_svc->part_export ) {
256 #this will catch to the same exact export
257 my @svcparts = map { $_->svcpart }
258 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
260 #this will catch to exports w/same exporthost+type ???
261 #my @other_part_export = qsearch('part_export', {
262 # 'machine' => $part_export->machine,
263 # 'exporttype' => $part_export->exporttype,
265 #foreach my $other_part_export ( @other_part_export ) {
266 # push @svcparts, map { $_->svcpart }
267 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
270 my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
271 if ( $nodomain =~ /^Y/i ) {
272 $conflict_user_svcpart{$_} = $part_export->exportnum
275 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
280 foreach my $dup_user ( @dup_user ) {
281 my $dup_svcpart = $dup_user->cust_svc->svcpart;
282 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
283 $dbh->rollback if $oldAutoCommit;
284 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
285 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
289 foreach my $dup_userdomain ( @dup_userdomain ) {
290 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
291 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
292 $dbh->rollback if $oldAutoCommit;
293 return "duplicate username\@domain: conflicts with svcnum ".
294 $dup_userdomain->svcnum. " via exportnum ".
295 $conflict_userdomain_svcpart{$dup_svcpart};
299 foreach my $dup_uid ( @dup_uid ) {
300 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
301 if ( exists($conflict_user_svcpart{$dup_svcpart})
302 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
303 $dbh->rollback if $oldAutoCommit;
304 return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
305 "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
306 || $conflict_userdomain_svcpart{$dup_svcpart};
312 #see? i told you it was more complicated
315 $error = $self->SUPER::insert(\@jobnums);
317 $dbh->rollback if $oldAutoCommit;
321 if ( $self->usergroup ) {
322 foreach my $groupname ( @{$self->usergroup} ) {
323 my $radius_usergroup = new FS::radius_usergroup ( {
324 svcnum => $self->svcnum,
325 groupname => $groupname,
327 my $error = $radius_usergroup->insert;
329 $dbh->rollback if $oldAutoCommit;
335 #false laziness with sub replace (and cust_main)
336 my $queue = new FS::queue {
337 'svcnum' => $self->svcnum,
338 'job' => 'FS::svc_acct::append_fuzzyfiles'
340 $error = $queue->insert($self->username);
342 $dbh->rollback if $oldAutoCommit;
343 return "queueing job (transaction rolled back): $error";
346 my $cust_pkg = $self->cust_svc->cust_pkg;
349 my $cust_main = $cust_pkg->cust_main;
351 if ( $conf->exists('emailinvoiceauto') ) {
352 my @invoicing_list = $cust_main->invoicing_list;
353 push @invoicing_list, $self->email;
354 $cust_main->invoicing_list(\@invoicing_list);
359 if ( $welcome_template && $cust_pkg ) {
360 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
362 my $wqueue = new FS::queue {
363 'svcnum' => $self->svcnum,
364 'job' => 'FS::svc_acct::send_email'
366 warn "attempting to queue email to $to";
367 my $error = $wqueue->insert(
369 'from' => $welcome_from,
370 'subject' => $welcome_subject,
371 'mimetype' => $welcome_mimetype,
372 'body' => $welcome_template->fill_in( HASH => {
373 'username' => $self->username,
374 'password' => $self->_password,
375 'first' => $cust_main->first,
376 'last' => $cust_main->getfield('last'),
377 'pkg' => $cust_pkg->part_pkg->pkg,
381 $dbh->rollback if $oldAutoCommit;
382 return "queuing welcome email: $error";
385 foreach my $jobnum ( @jobnums ) {
386 my $error = $wqueue->depend_insert($jobnum);
388 $dbh->rollback if $oldAutoCommit;
389 return "queuing welcome email job dependancy: $error";
399 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
405 Deletes this account from the database. If there is an error, returns the
406 error, otherwise returns false.
408 The corresponding FS::cust_svc record will be deleted as well.
410 (TODOC: new exports! $noexport_hack)
417 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
418 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
419 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
422 return "Can't delete an account which is a (svc_forward) source!"
423 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
425 return "Can't delete an account which is a (svc_forward) destination!"
426 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
428 return "Can't delete an account with (svc_www) web service!"
429 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
431 # what about records in session ? (they should refer to history table)
433 local $SIG{HUP} = 'IGNORE';
434 local $SIG{INT} = 'IGNORE';
435 local $SIG{QUIT} = 'IGNORE';
436 local $SIG{TERM} = 'IGNORE';
437 local $SIG{TSTP} = 'IGNORE';
438 local $SIG{PIPE} = 'IGNORE';
440 my $oldAutoCommit = $FS::UID::AutoCommit;
441 local $FS::UID::AutoCommit = 0;
444 foreach my $cust_main_invoice (
445 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
447 unless ( defined($cust_main_invoice) ) {
448 warn "WARNING: something's wrong with qsearch";
451 my %hash = $cust_main_invoice->hash;
452 $hash{'dest'} = $self->email;
453 my $new = new FS::cust_main_invoice \%hash;
454 my $error = $new->replace($cust_main_invoice);
456 $dbh->rollback if $oldAutoCommit;
461 foreach my $svc_domain (
462 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
464 my %hash = new FS::svc_domain->hash;
465 $hash{'catchall'} = '';
466 my $new = new FS::svc_domain \%hash;
467 my $error = $new->replace($svc_domain);
469 $dbh->rollback if $oldAutoCommit;
474 foreach my $radius_usergroup (
475 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
477 my $error = $radius_usergroup->delete;
479 $dbh->rollback if $oldAutoCommit;
484 my $error = $self->SUPER::delete;
486 $dbh->rollback if $oldAutoCommit;
490 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
494 =item replace OLD_RECORD
496 Replaces OLD_RECORD with this one in the database. If there is an error,
497 returns the error, otherwise returns false.
499 The additional field I<usergroup> can optionally be defined; if so it should
500 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
501 sqlradius export only)
506 my ( $new, $old ) = ( shift, shift );
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 $old->usergroup( [ $old->radius_groups ] );
535 if ( $new->usergroup ) {
536 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
537 my @newgroups = @{$new->usergroup};
538 foreach my $oldgroup ( @{$old->usergroup} ) {
539 if ( grep { $oldgroup eq $_ } @newgroups ) {
540 @newgroups = grep { $oldgroup ne $_ } @newgroups;
543 my $radius_usergroup = qsearchs('radius_usergroup', {
544 svcnum => $old->svcnum,
545 groupname => $oldgroup,
547 my $error = $radius_usergroup->delete;
549 $dbh->rollback if $oldAutoCommit;
550 return "error deleting radius_usergroup $oldgroup: $error";
554 foreach my $newgroup ( @newgroups ) {
555 my $radius_usergroup = new FS::radius_usergroup ( {
556 svcnum => $new->svcnum,
557 groupname => $newgroup,
559 my $error = $radius_usergroup->insert;
561 $dbh->rollback if $oldAutoCommit;
562 return "error adding radius_usergroup $newgroup: $error";
568 $error = $new->SUPER::replace($old);
570 $dbh->rollback if $oldAutoCommit;
571 return $error if $error;
574 if ( $new->username ne $old->username ) {
575 #false laziness with sub insert (and cust_main)
576 my $queue = new FS::queue {
577 'svcnum' => $new->svcnum,
578 'job' => 'FS::svc_acct::append_fuzzyfiles'
580 $error = $queue->insert($new->username);
582 $dbh->rollback if $oldAutoCommit;
583 return "queueing job (transaction rolled back): $error";
587 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
593 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
594 error, returns the error, otherwise returns false.
596 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
602 my %hash = $self->hash;
603 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
604 || $hash{_password} eq '*'
606 $hash{_password} = '*SUSPENDED* '.$hash{_password};
607 my $new = new FS::svc_acct ( \%hash );
608 my $error = $new->replace($self);
609 return $error if $error;
612 $self->SUPER::suspend;
617 Unsuspends this account by removing *SUSPENDED* from the password. If there is
618 an error, returns the error, otherwise returns false.
620 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
626 my %hash = $self->hash;
627 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
628 $hash{_password} = $1;
629 my $new = new FS::svc_acct ( \%hash );
630 my $error = $new->replace($self);
631 return $error if $error;
634 $self->SUPER::unsuspend;
639 Just returns false (no error) for now.
641 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
645 Checks all fields to make sure this is a valid service. If there is an error,
646 returns the error, otherwise returns false. Called by the insert and replace
649 Sets any fixed values; see L<FS::part_svc>.
656 my($recref) = $self->hashref;
658 my $x = $self->setfixed;
659 return $x unless ref($x);
662 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
664 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
667 my $error = $self->ut_numbern('svcnum')
668 || $self->ut_number('domsvc')
669 || $self->ut_textn('sec_phrase')
671 return $error if $error;
673 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
674 if ( $username_uppercase ) {
675 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
676 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
677 $recref->{username} = $1;
679 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
680 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
681 $recref->{username} = $1;
684 if ( $username_letterfirst ) {
685 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
686 } elsif ( $username_letter ) {
687 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
689 if ( $username_noperiod ) {
690 $recref->{username} =~ /\./ and return gettext('illegal_username');
692 if ( $username_nounderscore ) {
693 $recref->{username} =~ /_/ and return gettext('illegal_username');
695 if ( $username_nodash ) {
696 $recref->{username} =~ /\-/ and return gettext('illegal_username');
698 unless ( $username_ampersand ) {
699 $recref->{username} =~ /\&/ and return gettext('illegal_username');
702 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
703 $recref->{popnum} = $1;
704 return "Unknown popnum" unless
705 ! $recref->{popnum} ||
706 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
708 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
710 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
711 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
713 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
714 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
715 #not all systems use gid=uid
716 #you can set a fixed gid in part_svc
718 return "Only root can have uid 0"
719 if $recref->{uid} == 0
720 && $recref->{username} ne 'root'
721 && $recref->{username} ne 'toor';
724 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
725 or return "Illegal directory: ". $recref->{dir};
727 return "Illegal directory"
728 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
729 return "Illegal directory"
730 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
731 unless ( $recref->{dir} ) {
732 $recref->{dir} = $dir_prefix . '/';
733 if ( $dirhash > 0 ) {
734 for my $h ( 1 .. $dirhash ) {
735 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
737 } elsif ( $dirhash < 0 ) {
738 for my $h ( reverse $dirhash .. -1 ) {
739 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
742 $recref->{dir} .= $recref->{username};
746 unless ( $recref->{username} eq 'sync' ) {
747 if ( grep $_ eq $recref->{shell}, @shells ) {
748 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
750 return "Illegal shell \`". $self->shell. "\'; ".
751 $conf->dir. "/shells contains: @shells";
754 $recref->{shell} = '/bin/sync';
758 $recref->{gid} ne '' ?
759 return "Can't have gid without uid" : ( $recref->{gid}='' );
760 $recref->{dir} ne '' ?
761 return "Can't have directory without uid" : ( $recref->{dir}='' );
762 $recref->{shell} ne '' ?
763 return "Can't have shell without uid" : ( $recref->{shell}='' );
766 # $error = $self->ut_textn('finger');
767 # return $error if $error;
768 $self->getfield('finger') =~
769 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
770 or return "Illegal finger: ". $self->getfield('finger');
771 $self->setfield('finger', $1);
773 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
774 $recref->{quota} = $1;
776 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
777 unless ( $recref->{slipip} eq '0e0' ) {
778 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
779 or return "Illegal slipip". $self->slipip;
780 $recref->{slipip} = $1;
782 $recref->{slipip} = '0e0';
787 #arbitrary RADIUS stuff; allow ut_textn for now
788 foreach ( grep /^radius_/, fields('svc_acct') ) {
792 #generate a password if it is blank
793 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
794 unless ( $recref->{_password} );
796 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
797 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
798 $recref->{_password} = $1.$3;
799 #uncomment this to encrypt password immediately upon entry, or run
800 #bin/crypt_pw in cron to give new users a window during which their
801 #password is available to techs, for faxing, etc. (also be aware of
803 #$recref->{password} = $1.
804 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
806 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
807 $recref->{_password} = $1.$3;
808 } elsif ( $recref->{_password} eq '*' ) {
809 $recref->{_password} = '*';
810 } elsif ( $recref->{_password} eq '!!' ) {
811 $recref->{_password} = '!!';
813 #return "Illegal password";
814 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
815 FS::Msgcat::_gettext('illegal_password_characters').
816 ": ". $recref->{_password};
824 Depriciated, use radius_reply instead.
829 carp "FS::svc_acct::radius depriciated, use radius_reply";
835 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
836 reply attributes of this record.
838 Note that this is now the preferred method for reading RADIUS attributes -
839 accessing the columns directly is discouraged, as the column names are
840 expected to change in the future.
849 my($column, $attrib) = ($1, $2);
850 #$attrib =~ s/_/\-/g;
851 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
852 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
853 if ( $self->slipip && $self->slipip ne '0e0' ) {
854 $reply{'Framed-IP-Address'} = $self->slipip;
861 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
862 check attributes of this record.
864 Note that this is now the preferred method for reading RADIUS attributes -
865 accessing the columns directly is discouraged, as the column names are
866 expected to change in the future.
872 my $password = $self->_password;
873 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
874 ( $pw_attrib => $self->_password,
877 my($column, $attrib) = ($1, $2);
878 #$attrib =~ s/_/\-/g;
879 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
880 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
886 Returns the domain associated with this account.
892 if ( $self->domsvc ) {
893 #$self->svc_domain->domain;
894 my $svc_domain = $self->svc_domain
895 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
898 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
904 Returns the FS::svc_domain record for this account's domain (see
913 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
918 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
922 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
927 Returns an email address associated with the account.
933 $self->username. '@'. $self->domain;
936 =item seconds_since TIMESTAMP
938 Returns the number of seconds this account has been online since TIMESTAMP,
939 according to the session monitor (see L<FS::Session>).
941 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
942 L<Time::Local> and L<Date::Parse> for conversion functions.
946 #note: POD here, implementation in FS::cust_svc
949 $self->cust_svc->seconds_since(@_);
952 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
954 Returns the numbers of seconds this account has been online between
955 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
956 external SQL radacct table, specified via sqlradius export. Sessions which
957 started in the specified range but are still open are counted from session
958 start to the end of the range (unless they are over 1 day old, in which case
959 they are presumed missing their stop record and not counted). Also, sessions
960 which end in the range but started earlier are counted from the start of the
961 range to session end. Finally, sessions which start before the range but end
962 after are counted for the entire range.
964 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
965 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
970 #note: POD here, implementation in FS::cust_svc
971 sub seconds_since_sqlradacct {
973 $self->cust_svc->seconds_since_sqlradacct(@_);
976 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
978 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
979 in this package for sessions ending between TIMESTAMP_START (inclusive) and
980 TIMESTAMP_END (exclusive).
982 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
983 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
988 #note: POD here, implementation in FS::cust_svc
989 sub attribute_since_sqlradacct {
991 $self->cust_svc->attribute_since_sqlradacct(@_);
997 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1003 if ( $self->usergroup ) {
1004 #when provisioning records, export callback runs in svc_Common.pm before
1005 #radius_usergroup records can be inserted...
1006 @{$self->usergroup};
1008 map { $_->groupname }
1009 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1027 use Mail::Internet 1.44;
1030 $opt{mimetype} ||= 'text/plain';
1031 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1033 $ENV{MAILADDRESS} = $opt{from};
1034 my $header = new Mail::Header ( [
1037 "Sender: $opt{from}",
1038 "Reply-To: $opt{from}",
1039 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1040 "Subject: $opt{subject}",
1041 "Content-Type: $opt{mimetype}",
1043 my $message = new Mail::Internet (
1044 'Header' => $header,
1045 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1048 $message->smtpsend( Host => $smtpmachine )
1049 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1050 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1053 =item check_and_rebuild_fuzzyfiles
1057 sub check_and_rebuild_fuzzyfiles {
1058 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1059 -e "$dir/svc_acct.username"
1060 or &rebuild_fuzzyfiles;
1063 =item rebuild_fuzzyfiles
1067 sub rebuild_fuzzyfiles {
1069 use Fcntl qw(:flock);
1071 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1075 open(USERNAMELOCK,">>$dir/svc_acct.username")
1076 or die "can't open $dir/svc_acct.username: $!";
1077 flock(USERNAMELOCK,LOCK_EX)
1078 or die "can't lock $dir/svc_acct.username: $!";
1080 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1082 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1083 or die "can't open $dir/svc_acct.username.tmp: $!";
1084 print USERNAMECACHE join("\n", @all_username), "\n";
1085 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1087 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1097 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1098 open(USERNAMECACHE,"<$dir/svc_acct.username")
1099 or die "can't open $dir/svc_acct.username: $!";
1100 my @array = map { chomp; $_; } <USERNAMECACHE>;
1101 close USERNAMECACHE;
1105 =item append_fuzzyfiles USERNAME
1109 sub append_fuzzyfiles {
1110 my $username = shift;
1112 &check_and_rebuild_fuzzyfiles;
1114 use Fcntl qw(:flock);
1116 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1118 open(USERNAME,">>$dir/svc_acct.username")
1119 or die "can't open $dir/svc_acct.username: $!";
1120 flock(USERNAME,LOCK_EX)
1121 or die "can't lock $dir/svc_acct.username: $!";
1123 print USERNAME "$username\n";
1125 flock(USERNAME,LOCK_UN)
1126 or die "can't unlock $dir/svc_acct.username: $!";
1134 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1138 sub radius_usergroup_selector {
1139 my $sel_groups = shift;
1140 my %sel_groups = map { $_=>1 } @$sel_groups;
1142 my $selectname = shift || 'radius_usergroup';
1145 my $sth = $dbh->prepare(
1146 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1147 ) or die $dbh->errstr;
1148 $sth->execute() or die $sth->errstr;
1149 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1153 function ${selectname}_doadd(object) {
1154 var myvalue = object.${selectname}_add.value;
1155 var optionName = new Option(myvalue,myvalue,false,true);
1156 var length = object.$selectname.length;
1157 object.$selectname.options[length] = optionName;
1158 object.${selectname}_add.value = "";
1161 <SELECT MULTIPLE NAME="$selectname">
1164 foreach my $group ( @all_groups ) {
1166 if ( $sel_groups{$group} ) {
1167 $html .= ' SELECTED';
1168 $sel_groups{$group} = 0;
1170 $html .= ">$group</OPTION>\n";
1172 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1173 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1175 $html .= '</SELECT>';
1177 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1178 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1187 The $recref stuff in sub check should be cleaned up.
1189 The suspend, unsuspend and cancel methods update the database, but not the
1190 current object. This is probably a bug as it's unexpected and
1193 radius_usergroup_selector? putting web ui components in here? they should
1194 probably live somewhere else...
1198 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1199 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1200 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1201 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1202 schema.html from the base documentation.