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
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 $welcome_subject = '';
71 $welcome_mimetype = '';
73 $smtpmachine = $conf->config('smtpmachine');
74 $radius_password = $conf->config('radius-password') || 'Password';
77 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
78 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
82 my ( $hashref, $cache ) = @_;
83 if ( $hashref->{'svc_acct_svcnum'} ) {
84 $self->{'_domsvc'} = FS::svc_domain->new( {
85 'svcnum' => $hashref->{'domsvc'},
86 'domain' => $hashref->{'svc_acct_domain'},
87 'catchall' => $hashref->{'svc_acct_catchall'},
94 FS::svc_acct - Object methods for svc_acct records
100 $record = new FS::svc_acct \%hash;
101 $record = new FS::svc_acct { 'column' => 'value' };
103 $error = $record->insert;
105 $error = $new_record->replace($old_record);
107 $error = $record->delete;
109 $error = $record->check;
111 $error = $record->suspend;
113 $error = $record->unsuspend;
115 $error = $record->cancel;
117 %hash = $record->radius;
119 %hash = $record->radius_reply;
121 %hash = $record->radius_check;
123 $domain = $record->domain;
125 $svc_domain = $record->svc_domain;
127 $email = $record->email;
129 $seconds_since = $record->seconds_since($timestamp);
133 An FS::svc_acct object represents an account. FS::svc_acct inherits from
134 FS::svc_Common. The following fields are currently supported:
138 =item svcnum - primary key (assigned automatcially for new accounts)
142 =item _password - generated if blank
144 =item sec_phrase - security phrase
146 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
154 =item dir - set automatically if blank (and uid is not)
158 =item quota - (unimplementd)
160 =item slipip - IP address
164 =item domsvc - svcnum from svc_domain
166 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
176 Creates a new account. To add the account to the database, see L<"insert">.
180 sub table { 'svc_acct'; }
184 Adds this account to the database. If there is an error, returns the error,
185 otherwise returns false.
187 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
188 defined. An FS::cust_svc record will be created and inserted.
190 The additional field I<usergroup> can optionally be defined; if so it should
191 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
192 sqlradius export only)
194 (TODOC: L<FS::queue> and L<freeside-queued>)
196 (TODOC: new exports!)
204 local $SIG{HUP} = 'IGNORE';
205 local $SIG{INT} = 'IGNORE';
206 local $SIG{QUIT} = 'IGNORE';
207 local $SIG{TERM} = 'IGNORE';
208 local $SIG{TSTP} = 'IGNORE';
209 local $SIG{PIPE} = 'IGNORE';
211 my $oldAutoCommit = $FS::UID::AutoCommit;
212 local $FS::UID::AutoCommit = 0;
215 $error = $self->check;
216 return $error if $error;
218 #no, duplicate checking just got a whole lot more complicated
219 #(perhaps keep this check with a config option to turn on?)
221 #return gettext('username_in_use'). ": ". $self->username
222 # if qsearchs( 'svc_acct', { 'username' => $self->username,
223 # 'domsvc' => $self->domsvc,
226 if ( $self->svcnum ) {
227 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
228 unless ( $cust_svc ) {
229 $dbh->rollback if $oldAutoCommit;
230 return "no cust_svc record found for svcnum ". $self->svcnum;
232 $self->pkgnum($cust_svc->pkgnum);
233 $self->svcpart($cust_svc->svcpart);
236 #new duplicate username checking
238 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
239 unless ( $part_svc ) {
240 $dbh->rollback if $oldAutoCommit;
241 return 'unknown svcpart '. $self->svcpart;
244 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
245 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
246 'domsvc' => $self->domsvc } );
248 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
249 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
250 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
255 if ( @dup_user || @dup_userdomain || @dup_uid ) {
256 my $exports = FS::part_export::export_info('svc_acct');
257 my %conflict_user_svcpart;
258 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
260 foreach my $part_export ( $part_svc->part_export ) {
262 #this will catch to the same exact export
263 my @svcparts = map { $_->svcpart }
264 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
266 #this will catch to exports w/same exporthost+type ???
267 #my @other_part_export = qsearch('part_export', {
268 # 'machine' => $part_export->machine,
269 # 'exporttype' => $part_export->exporttype,
271 #foreach my $other_part_export ( @other_part_export ) {
272 # push @svcparts, map { $_->svcpart }
273 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
276 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
277 #silly kludge to avoid uninitialized value errors
278 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
279 ? $exports->{$part_export->exporttype}{'nodomain'}
281 if ( $nodomain =~ /^Y/i ) {
282 $conflict_user_svcpart{$_} = $part_export->exportnum
285 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
290 foreach my $dup_user ( @dup_user ) {
291 my $dup_svcpart = $dup_user->cust_svc->svcpart;
292 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
293 $dbh->rollback if $oldAutoCommit;
294 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
295 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
299 foreach my $dup_userdomain ( @dup_userdomain ) {
300 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
301 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
302 $dbh->rollback if $oldAutoCommit;
303 return "duplicate username\@domain: conflicts with svcnum ".
304 $dup_userdomain->svcnum. " via exportnum ".
305 $conflict_userdomain_svcpart{$dup_svcpart};
309 foreach my $dup_uid ( @dup_uid ) {
310 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
311 if ( exists($conflict_user_svcpart{$dup_svcpart})
312 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
313 $dbh->rollback if $oldAutoCommit;
314 return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
315 "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
316 || $conflict_userdomain_svcpart{$dup_svcpart};
322 #see? i told you it was more complicated
325 $error = $self->SUPER::insert(\@jobnums);
327 $dbh->rollback if $oldAutoCommit;
331 if ( $self->usergroup ) {
332 foreach my $groupname ( @{$self->usergroup} ) {
333 my $radius_usergroup = new FS::radius_usergroup ( {
334 svcnum => $self->svcnum,
335 groupname => $groupname,
337 my $error = $radius_usergroup->insert;
339 $dbh->rollback if $oldAutoCommit;
345 #false laziness with sub replace (and cust_main)
346 my $queue = new FS::queue {
347 'svcnum' => $self->svcnum,
348 'job' => 'FS::svc_acct::append_fuzzyfiles'
350 $error = $queue->insert($self->username);
352 $dbh->rollback if $oldAutoCommit;
353 return "queueing job (transaction rolled back): $error";
356 my $cust_pkg = $self->cust_svc->cust_pkg;
359 my $cust_main = $cust_pkg->cust_main;
361 if ( $conf->exists('emailinvoiceauto') ) {
362 my @invoicing_list = $cust_main->invoicing_list;
363 push @invoicing_list, $self->email;
364 $cust_main->invoicing_list(\@invoicing_list);
369 if ( $welcome_template && $cust_pkg ) {
370 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
372 my $wqueue = new FS::queue {
373 'svcnum' => $self->svcnum,
374 'job' => 'FS::svc_acct::send_email'
376 my $error = $wqueue->insert(
378 'from' => $welcome_from,
379 'subject' => $welcome_subject,
380 'mimetype' => $welcome_mimetype,
381 'body' => $welcome_template->fill_in( HASH => {
382 'custnum' => $self->custnum,
383 'username' => $self->username,
384 'password' => $self->_password,
385 'first' => $cust_main->first,
386 'last' => $cust_main->getfield('last'),
387 'pkg' => $cust_pkg->part_pkg->pkg,
391 $dbh->rollback if $oldAutoCommit;
392 return "error queuing welcome email: $error";
395 foreach my $jobnum ( @jobnums ) {
396 my $error = $wqueue->depend_insert($jobnum);
398 $dbh->rollback if $oldAutoCommit;
399 return "error queuing welcome email job dependancy: $error";
409 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
415 Deletes this account from the database. If there is an error, returns the
416 error, otherwise returns false.
418 The corresponding FS::cust_svc record will be deleted as well.
420 (TODOC: new exports!)
427 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
428 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
429 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
432 return "Can't delete an account which is a (svc_forward) source!"
433 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
435 return "Can't delete an account which is a (svc_forward) destination!"
436 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
438 return "Can't delete an account with (svc_www) web service!"
439 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
441 # what about records in session ? (they should refer to history table)
443 local $SIG{HUP} = 'IGNORE';
444 local $SIG{INT} = 'IGNORE';
445 local $SIG{QUIT} = 'IGNORE';
446 local $SIG{TERM} = 'IGNORE';
447 local $SIG{TSTP} = 'IGNORE';
448 local $SIG{PIPE} = 'IGNORE';
450 my $oldAutoCommit = $FS::UID::AutoCommit;
451 local $FS::UID::AutoCommit = 0;
454 foreach my $cust_main_invoice (
455 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
457 unless ( defined($cust_main_invoice) ) {
458 warn "WARNING: something's wrong with qsearch";
461 my %hash = $cust_main_invoice->hash;
462 $hash{'dest'} = $self->email;
463 my $new = new FS::cust_main_invoice \%hash;
464 my $error = $new->replace($cust_main_invoice);
466 $dbh->rollback if $oldAutoCommit;
471 foreach my $svc_domain (
472 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
474 my %hash = new FS::svc_domain->hash;
475 $hash{'catchall'} = '';
476 my $new = new FS::svc_domain \%hash;
477 my $error = $new->replace($svc_domain);
479 $dbh->rollback if $oldAutoCommit;
484 foreach my $radius_usergroup (
485 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
487 my $error = $radius_usergroup->delete;
489 $dbh->rollback if $oldAutoCommit;
494 my $error = $self->SUPER::delete;
496 $dbh->rollback if $oldAutoCommit;
500 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
504 =item replace OLD_RECORD
506 Replaces OLD_RECORD with this one in the database. If there is an error,
507 returns the error, otherwise returns false.
509 The additional field I<usergroup> can optionally be defined; if so it should
510 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
511 sqlradius export only)
516 my ( $new, $old ) = ( shift, shift );
518 warn "$me replacing $old with $new\n" if $DEBUG;
520 return "Username in use"
521 if $old->username ne $new->username &&
522 qsearchs( 'svc_acct', { 'username' => $new->username,
523 'domsvc' => $new->domsvc,
526 #no warnings 'numeric'; #alas, a 5.006-ism
528 return "Can't change uid!" if $old->uid != $new->uid;
531 #change homdir when we change username
532 $new->setfield('dir', '') if $old->username ne $new->username;
534 local $SIG{HUP} = 'IGNORE';
535 local $SIG{INT} = 'IGNORE';
536 local $SIG{QUIT} = 'IGNORE';
537 local $SIG{TERM} = 'IGNORE';
538 local $SIG{TSTP} = 'IGNORE';
539 local $SIG{PIPE} = 'IGNORE';
541 my $oldAutoCommit = $FS::UID::AutoCommit;
542 local $FS::UID::AutoCommit = 0;
545 # redundant, but so $new->usergroup gets set
546 $error = $new->check;
547 return $error if $error;
549 $old->usergroup( [ $old->radius_groups ] );
550 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
551 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
552 if ( $new->usergroup ) {
553 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
554 my @newgroups = @{$new->usergroup};
555 foreach my $oldgroup ( @{$old->usergroup} ) {
556 if ( grep { $oldgroup eq $_ } @newgroups ) {
557 @newgroups = grep { $oldgroup ne $_ } @newgroups;
560 my $radius_usergroup = qsearchs('radius_usergroup', {
561 svcnum => $old->svcnum,
562 groupname => $oldgroup,
564 my $error = $radius_usergroup->delete;
566 $dbh->rollback if $oldAutoCommit;
567 return "error deleting radius_usergroup $oldgroup: $error";
571 foreach my $newgroup ( @newgroups ) {
572 my $radius_usergroup = new FS::radius_usergroup ( {
573 svcnum => $new->svcnum,
574 groupname => $newgroup,
576 my $error = $radius_usergroup->insert;
578 $dbh->rollback if $oldAutoCommit;
579 return "error adding radius_usergroup $newgroup: $error";
585 $error = $new->SUPER::replace($old);
587 $dbh->rollback if $oldAutoCommit;
588 return $error if $error;
591 if ( $new->username ne $old->username ) {
592 #false laziness with sub insert (and cust_main)
593 my $queue = new FS::queue {
594 'svcnum' => $new->svcnum,
595 'job' => 'FS::svc_acct::append_fuzzyfiles'
597 $error = $queue->insert($new->username);
599 $dbh->rollback if $oldAutoCommit;
600 return "queueing job (transaction rolled back): $error";
604 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
610 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
611 error, returns the error, otherwise returns false.
613 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
619 my %hash = $self->hash;
620 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
621 || $hash{_password} eq '*'
623 $hash{_password} = '*SUSPENDED* '.$hash{_password};
624 my $new = new FS::svc_acct ( \%hash );
625 my $error = $new->replace($self);
626 return $error if $error;
629 $self->SUPER::suspend;
634 Unsuspends this account by removing *SUSPENDED* from the password. If there is
635 an error, returns the error, otherwise returns false.
637 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
643 my %hash = $self->hash;
644 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
645 $hash{_password} = $1;
646 my $new = new FS::svc_acct ( \%hash );
647 my $error = $new->replace($self);
648 return $error if $error;
651 $self->SUPER::unsuspend;
656 Just returns false (no error) for now.
658 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
662 Checks all fields to make sure this is a valid service. If there is an error,
663 returns the error, otherwise returns false. Called by the insert and replace
666 Sets any fixed values; see L<FS::part_svc>.
673 my($recref) = $self->hashref;
675 my $x = $self->setfixed;
676 return $x unless ref($x);
679 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
681 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
684 my $error = $self->ut_numbern('svcnum')
685 || $self->ut_number('domsvc')
686 || $self->ut_textn('sec_phrase')
688 return $error if $error;
690 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
691 if ( $username_uppercase ) {
692 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
693 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
694 $recref->{username} = $1;
696 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
697 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
698 $recref->{username} = $1;
701 if ( $username_letterfirst ) {
702 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
703 } elsif ( $username_letter ) {
704 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
706 if ( $username_noperiod ) {
707 $recref->{username} =~ /\./ and return gettext('illegal_username');
709 if ( $username_nounderscore ) {
710 $recref->{username} =~ /_/ and return gettext('illegal_username');
712 if ( $username_nodash ) {
713 $recref->{username} =~ /\-/ and return gettext('illegal_username');
715 unless ( $username_ampersand ) {
716 $recref->{username} =~ /\&/ and return gettext('illegal_username');
719 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
720 $recref->{popnum} = $1;
721 return "Unknown popnum" unless
722 ! $recref->{popnum} ||
723 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
725 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
727 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
728 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
730 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
731 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
732 #not all systems use gid=uid
733 #you can set a fixed gid in part_svc
735 return "Only root can have uid 0"
736 if $recref->{uid} == 0
737 && $recref->{username} ne 'root'
738 && $recref->{username} ne 'toor';
741 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
742 or return "Illegal directory: ". $recref->{dir};
744 return "Illegal directory"
745 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
746 return "Illegal directory"
747 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
748 unless ( $recref->{dir} ) {
749 $recref->{dir} = $dir_prefix . '/';
750 if ( $dirhash > 0 ) {
751 for my $h ( 1 .. $dirhash ) {
752 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
754 } elsif ( $dirhash < 0 ) {
755 for my $h ( reverse $dirhash .. -1 ) {
756 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
759 $recref->{dir} .= $recref->{username};
763 unless ( $recref->{username} eq 'sync' ) {
764 if ( grep $_ eq $recref->{shell}, @shells ) {
765 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
767 return "Illegal shell \`". $self->shell. "\'; ".
768 $conf->dir. "/shells contains: @shells";
771 $recref->{shell} = '/bin/sync';
775 $recref->{gid} ne '' ?
776 return "Can't have gid without uid" : ( $recref->{gid}='' );
777 $recref->{dir} ne '' ?
778 return "Can't have directory without uid" : ( $recref->{dir}='' );
779 $recref->{shell} ne '' ?
780 return "Can't have shell without uid" : ( $recref->{shell}='' );
783 # $error = $self->ut_textn('finger');
784 # return $error if $error;
785 $self->getfield('finger') =~
786 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
787 or return "Illegal finger: ". $self->getfield('finger');
788 $self->setfield('finger', $1);
790 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
791 $recref->{quota} = $1;
793 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
794 unless ( $recref->{slipip} eq '0e0' ) {
795 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
796 or return "Illegal slipip". $self->slipip;
797 $recref->{slipip} = $1;
799 $recref->{slipip} = '0e0';
804 #arbitrary RADIUS stuff; allow ut_textn for now
805 foreach ( grep /^radius_/, fields('svc_acct') ) {
809 #generate a password if it is blank
810 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
811 unless ( $recref->{_password} );
813 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
814 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
815 $recref->{_password} = $1.$3;
816 #uncomment this to encrypt password immediately upon entry, or run
817 #bin/crypt_pw in cron to give new users a window during which their
818 #password is available to techs, for faxing, etc. (also be aware of
820 #$recref->{password} = $1.
821 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
823 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
824 $recref->{_password} = $1.$3;
825 } elsif ( $recref->{_password} eq '*' ) {
826 $recref->{_password} = '*';
827 } elsif ( $recref->{_password} eq '!!' ) {
828 $recref->{_password} = '!!';
830 #return "Illegal password";
831 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
832 FS::Msgcat::_gettext('illegal_password_characters').
833 ": ". $recref->{_password};
841 Depriciated, use radius_reply instead.
846 carp "FS::svc_acct::radius depriciated, use radius_reply";
852 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
853 reply attributes of this record.
855 Note that this is now the preferred method for reading RADIUS attributes -
856 accessing the columns directly is discouraged, as the column names are
857 expected to change in the future.
866 my($column, $attrib) = ($1, $2);
867 #$attrib =~ s/_/\-/g;
868 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
869 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
870 if ( $self->slipip && $self->slipip ne '0e0' ) {
871 $reply{'Framed-IP-Address'} = $self->slipip;
878 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
879 check attributes of this record.
881 Note that this is now the preferred method for reading RADIUS attributes -
882 accessing the columns directly is discouraged, as the column names are
883 expected to change in the future.
889 my $password = $self->_password;
890 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
891 ( $pw_attrib => $self->_password,
894 my($column, $attrib) = ($1, $2);
895 #$attrib =~ s/_/\-/g;
896 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
897 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
903 Returns the domain associated with this account.
909 if ( $self->domsvc ) {
910 #$self->svc_domain->domain;
911 my $svc_domain = $self->svc_domain
912 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
915 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
921 Returns the FS::svc_domain record for this account's domain (see
930 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
935 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
939 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
944 Returns an email address associated with the account.
950 $self->username. '@'. $self->domain;
953 =item seconds_since TIMESTAMP
955 Returns the number of seconds this account has been online since TIMESTAMP,
956 according to the session monitor (see L<FS::Session>).
958 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
959 L<Time::Local> and L<Date::Parse> for conversion functions.
963 #note: POD here, implementation in FS::cust_svc
966 $self->cust_svc->seconds_since(@_);
969 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
971 Returns the numbers of seconds this account has been online between
972 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
973 external SQL radacct table, specified via sqlradius export. Sessions which
974 started in the specified range but are still open are counted from session
975 start to the end of the range (unless they are over 1 day old, in which case
976 they are presumed missing their stop record and not counted). Also, sessions
977 which end in the range but started earlier are counted from the start of the
978 range to session end. Finally, sessions which start before the range but end
979 after are counted for the entire range.
981 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
982 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
987 #note: POD here, implementation in FS::cust_svc
988 sub seconds_since_sqlradacct {
990 $self->cust_svc->seconds_since_sqlradacct(@_);
993 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
995 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
996 in this package for sessions ending between TIMESTAMP_START (inclusive) and
997 TIMESTAMP_END (exclusive).
999 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1000 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1005 #note: POD here, implementation in FS::cust_svc
1006 sub attribute_since_sqlradacct {
1008 $self->cust_svc->attribute_since_sqlradacct(@_);
1014 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1020 if ( $self->usergroup ) {
1021 #when provisioning records, export callback runs in svc_Common.pm before
1022 #radius_usergroup records can be inserted...
1023 @{$self->usergroup};
1025 map { $_->groupname }
1026 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1044 use Mail::Internet 1.44;
1047 $opt{mimetype} ||= 'text/plain';
1048 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1050 $ENV{MAILADDRESS} = $opt{from};
1051 my $header = new Mail::Header ( [
1054 "Sender: $opt{from}",
1055 "Reply-To: $opt{from}",
1056 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1057 "Subject: $opt{subject}",
1058 "Content-Type: $opt{mimetype}",
1060 my $message = new Mail::Internet (
1061 'Header' => $header,
1062 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1065 $message->smtpsend( Host => $smtpmachine )
1066 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1067 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1070 =item check_and_rebuild_fuzzyfiles
1074 sub check_and_rebuild_fuzzyfiles {
1075 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1076 -e "$dir/svc_acct.username"
1077 or &rebuild_fuzzyfiles;
1080 =item rebuild_fuzzyfiles
1084 sub rebuild_fuzzyfiles {
1086 use Fcntl qw(:flock);
1088 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1092 open(USERNAMELOCK,">>$dir/svc_acct.username")
1093 or die "can't open $dir/svc_acct.username: $!";
1094 flock(USERNAMELOCK,LOCK_EX)
1095 or die "can't lock $dir/svc_acct.username: $!";
1097 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1099 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1100 or die "can't open $dir/svc_acct.username.tmp: $!";
1101 print USERNAMECACHE join("\n", @all_username), "\n";
1102 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1104 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1114 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1115 open(USERNAMECACHE,"<$dir/svc_acct.username")
1116 or die "can't open $dir/svc_acct.username: $!";
1117 my @array = map { chomp; $_; } <USERNAMECACHE>;
1118 close USERNAMECACHE;
1122 =item append_fuzzyfiles USERNAME
1126 sub append_fuzzyfiles {
1127 my $username = shift;
1129 &check_and_rebuild_fuzzyfiles;
1131 use Fcntl qw(:flock);
1133 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1135 open(USERNAME,">>$dir/svc_acct.username")
1136 or die "can't open $dir/svc_acct.username: $!";
1137 flock(USERNAME,LOCK_EX)
1138 or die "can't lock $dir/svc_acct.username: $!";
1140 print USERNAME "$username\n";
1142 flock(USERNAME,LOCK_UN)
1143 or die "can't unlock $dir/svc_acct.username: $!";
1151 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1155 sub radius_usergroup_selector {
1156 my $sel_groups = shift;
1157 my %sel_groups = map { $_=>1 } @$sel_groups;
1159 my $selectname = shift || 'radius_usergroup';
1162 my $sth = $dbh->prepare(
1163 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1164 ) or die $dbh->errstr;
1165 $sth->execute() or die $sth->errstr;
1166 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1170 function ${selectname}_doadd(object) {
1171 var myvalue = object.${selectname}_add.value;
1172 var optionName = new Option(myvalue,myvalue,false,true);
1173 var length = object.$selectname.length;
1174 object.$selectname.options[length] = optionName;
1175 object.${selectname}_add.value = "";
1178 <SELECT MULTIPLE NAME="$selectname">
1181 foreach my $group ( @all_groups ) {
1183 if ( $sel_groups{$group} ) {
1184 $html .= ' SELECTED';
1185 $sel_groups{$group} = 0;
1187 $html .= ">$group</OPTION>\n";
1189 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1190 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1192 $html .= '</SELECT>';
1194 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1195 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1204 The $recref stuff in sub check should be cleaned up.
1206 The suspend, unsuspend and cancel methods update the database, but not the
1207 current object. This is probably a bug as it's unexpected and
1210 radius_usergroup_selector? putting web ui components in here? they should
1211 probably live somewhere else...
1215 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1216 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1217 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1218 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1219 schema.html from the base documentation.