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
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!)
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 my $error = $wqueue->insert(
371 'from' => $welcome_from,
372 'subject' => $welcome_subject,
373 'mimetype' => $welcome_mimetype,
374 'body' => $welcome_template->fill_in( HASH => {
375 'custnum' => $self->custnum,
376 'username' => $self->username,
377 'password' => $self->_password,
378 'first' => $cust_main->first,
379 'last' => $cust_main->getfield('last'),
380 'pkg' => $cust_pkg->part_pkg->pkg,
384 $dbh->rollback if $oldAutoCommit;
385 return "error queuing welcome email: $error";
388 foreach my $jobnum ( @jobnums ) {
389 my $error = $wqueue->depend_insert($jobnum);
391 $dbh->rollback if $oldAutoCommit;
392 return "error queuing welcome email job dependancy: $error";
402 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
408 Deletes this account from the database. If there is an error, returns the
409 error, otherwise returns false.
411 The corresponding FS::cust_svc record will be deleted as well.
413 (TODOC: new exports!)
420 return "Can't delete an account which is a (svc_forward) source!"
421 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
423 return "Can't delete an account which is a (svc_forward) destination!"
424 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
426 return "Can't delete an account with (svc_www) web service!"
427 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
429 # what about records in session ? (they should refer to history table)
431 local $SIG{HUP} = 'IGNORE';
432 local $SIG{INT} = 'IGNORE';
433 local $SIG{QUIT} = 'IGNORE';
434 local $SIG{TERM} = 'IGNORE';
435 local $SIG{TSTP} = 'IGNORE';
436 local $SIG{PIPE} = 'IGNORE';
438 my $oldAutoCommit = $FS::UID::AutoCommit;
439 local $FS::UID::AutoCommit = 0;
442 foreach my $cust_main_invoice (
443 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
445 unless ( defined($cust_main_invoice) ) {
446 warn "WARNING: something's wrong with qsearch";
449 my %hash = $cust_main_invoice->hash;
450 $hash{'dest'} = $self->email;
451 my $new = new FS::cust_main_invoice \%hash;
452 my $error = $new->replace($cust_main_invoice);
454 $dbh->rollback if $oldAutoCommit;
459 foreach my $svc_domain (
460 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
462 my %hash = new FS::svc_domain->hash;
463 $hash{'catchall'} = '';
464 my $new = new FS::svc_domain \%hash;
465 my $error = $new->replace($svc_domain);
467 $dbh->rollback if $oldAutoCommit;
472 foreach my $radius_usergroup (
473 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
475 my $error = $radius_usergroup->delete;
477 $dbh->rollback if $oldAutoCommit;
482 my $error = $self->SUPER::delete;
484 $dbh->rollback if $oldAutoCommit;
488 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
492 =item replace OLD_RECORD
494 Replaces OLD_RECORD with this one in the database. If there is an error,
495 returns the error, otherwise returns false.
497 The additional field I<usergroup> can optionally be defined; if so it should
498 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
499 sqlradius export only)
504 my ( $new, $old ) = ( shift, shift );
506 warn "$me replacing $old with $new\n" if $DEBUG;
508 return "Username in use"
509 if $old->username ne $new->username &&
510 qsearchs( 'svc_acct', { 'username' => $new->username,
511 'domsvc' => $new->domsvc,
514 #no warnings 'numeric'; #alas, a 5.006-ism
516 return "Can't change uid!" if $old->uid != $new->uid;
519 #change homdir when we change username
520 $new->setfield('dir', '') if $old->username ne $new->username;
522 local $SIG{HUP} = 'IGNORE';
523 local $SIG{INT} = 'IGNORE';
524 local $SIG{QUIT} = 'IGNORE';
525 local $SIG{TERM} = 'IGNORE';
526 local $SIG{TSTP} = 'IGNORE';
527 local $SIG{PIPE} = 'IGNORE';
529 my $oldAutoCommit = $FS::UID::AutoCommit;
530 local $FS::UID::AutoCommit = 0;
533 # redundant, but so $new->usergroup gets set
534 $error = $new->check;
535 return $error if $error;
537 $old->usergroup( [ $old->radius_groups ] );
538 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
539 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
540 if ( $new->usergroup ) {
541 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
542 my @newgroups = @{$new->usergroup};
543 foreach my $oldgroup ( @{$old->usergroup} ) {
544 if ( grep { $oldgroup eq $_ } @newgroups ) {
545 @newgroups = grep { $oldgroup ne $_ } @newgroups;
548 my $radius_usergroup = qsearchs('radius_usergroup', {
549 svcnum => $old->svcnum,
550 groupname => $oldgroup,
552 my $error = $radius_usergroup->delete;
554 $dbh->rollback if $oldAutoCommit;
555 return "error deleting radius_usergroup $oldgroup: $error";
559 foreach my $newgroup ( @newgroups ) {
560 my $radius_usergroup = new FS::radius_usergroup ( {
561 svcnum => $new->svcnum,
562 groupname => $newgroup,
564 my $error = $radius_usergroup->insert;
566 $dbh->rollback if $oldAutoCommit;
567 return "error adding radius_usergroup $newgroup: $error";
573 $error = $new->SUPER::replace($old);
575 $dbh->rollback if $oldAutoCommit;
576 return $error if $error;
579 if ( $new->username ne $old->username ) {
580 #false laziness with sub insert (and cust_main)
581 my $queue = new FS::queue {
582 'svcnum' => $new->svcnum,
583 'job' => 'FS::svc_acct::append_fuzzyfiles'
585 $error = $queue->insert($new->username);
587 $dbh->rollback if $oldAutoCommit;
588 return "queueing job (transaction rolled back): $error";
592 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
598 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
599 error, returns the error, otherwise returns false.
601 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
607 my %hash = $self->hash;
608 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
609 || $hash{_password} eq '*'
611 $hash{_password} = '*SUSPENDED* '.$hash{_password};
612 my $new = new FS::svc_acct ( \%hash );
613 my $error = $new->replace($self);
614 return $error if $error;
617 $self->SUPER::suspend;
622 Unsuspends this account by removing *SUSPENDED* from the password. If there is
623 an error, returns the error, otherwise returns false.
625 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
631 my %hash = $self->hash;
632 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
633 $hash{_password} = $1;
634 my $new = new FS::svc_acct ( \%hash );
635 my $error = $new->replace($self);
636 return $error if $error;
639 $self->SUPER::unsuspend;
644 Just returns false (no error) for now.
646 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
650 Checks all fields to make sure this is a valid service. If there is an error,
651 returns the error, otherwise returns false. Called by the insert and replace
654 Sets any fixed values; see L<FS::part_svc>.
661 my($recref) = $self->hashref;
663 my $x = $self->setfixed;
664 return $x unless ref($x);
667 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
669 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
672 my $error = $self->ut_numbern('svcnum')
673 || $self->ut_number('domsvc')
674 || $self->ut_textn('sec_phrase')
676 return $error if $error;
678 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
679 if ( $username_uppercase ) {
680 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
681 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
682 $recref->{username} = $1;
684 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
685 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
686 $recref->{username} = $1;
689 if ( $username_letterfirst ) {
690 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
691 } elsif ( $username_letter ) {
692 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
694 if ( $username_noperiod ) {
695 $recref->{username} =~ /\./ and return gettext('illegal_username');
697 if ( $username_nounderscore ) {
698 $recref->{username} =~ /_/ and return gettext('illegal_username');
700 if ( $username_nodash ) {
701 $recref->{username} =~ /\-/ and return gettext('illegal_username');
703 unless ( $username_ampersand ) {
704 $recref->{username} =~ /\&/ and return gettext('illegal_username');
707 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
708 $recref->{popnum} = $1;
709 return "Unknown popnum" unless
710 ! $recref->{popnum} ||
711 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
713 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
715 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
716 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
718 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
719 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
720 #not all systems use gid=uid
721 #you can set a fixed gid in part_svc
723 return "Only root can have uid 0"
724 if $recref->{uid} == 0
725 && $recref->{username} ne 'root'
726 && $recref->{username} ne 'toor';
729 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
730 or return "Illegal directory: ". $recref->{dir};
732 return "Illegal directory"
733 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
734 return "Illegal directory"
735 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
736 unless ( $recref->{dir} ) {
737 $recref->{dir} = $dir_prefix . '/';
738 if ( $dirhash > 0 ) {
739 for my $h ( 1 .. $dirhash ) {
740 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
742 } elsif ( $dirhash < 0 ) {
743 for my $h ( reverse $dirhash .. -1 ) {
744 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
747 $recref->{dir} .= $recref->{username};
751 unless ( $recref->{username} eq 'sync' ) {
752 if ( grep $_ eq $recref->{shell}, @shells ) {
753 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
755 return "Illegal shell \`". $self->shell. "\'; ".
756 $conf->dir. "/shells contains: @shells";
759 $recref->{shell} = '/bin/sync';
763 $recref->{gid} ne '' ?
764 return "Can't have gid without uid" : ( $recref->{gid}='' );
765 $recref->{dir} ne '' ?
766 return "Can't have directory without uid" : ( $recref->{dir}='' );
767 $recref->{shell} ne '' ?
768 return "Can't have shell without uid" : ( $recref->{shell}='' );
771 # $error = $self->ut_textn('finger');
772 # return $error if $error;
773 $self->getfield('finger') =~
774 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
775 or return "Illegal finger: ". $self->getfield('finger');
776 $self->setfield('finger', $1);
778 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
779 $recref->{quota} = $1;
781 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
782 unless ( $recref->{slipip} eq '0e0' ) {
783 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
784 or return "Illegal slipip: ". $self->slipip;
785 $recref->{slipip} = $1;
787 $recref->{slipip} = '0e0';
792 #arbitrary RADIUS stuff; allow ut_textn for now
793 foreach ( grep /^radius_/, fields('svc_acct') ) {
797 #generate a password if it is blank
798 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
799 unless ( $recref->{_password} );
801 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
802 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
803 $recref->{_password} = $1.$3;
804 #uncomment this to encrypt password immediately upon entry, or run
805 #bin/crypt_pw in cron to give new users a window during which their
806 #password is available to techs, for faxing, etc. (also be aware of
808 #$recref->{password} = $1.
809 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
811 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
812 $recref->{_password} = $1.$3;
813 } elsif ( $recref->{_password} eq '*' ) {
814 $recref->{_password} = '*';
815 } elsif ( $recref->{_password} eq '!!' ) {
816 $recref->{_password} = '!!';
818 #return "Illegal password";
819 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
820 FS::Msgcat::_gettext('illegal_password_characters').
821 ": ". $recref->{_password};
829 Depriciated, use radius_reply instead.
834 carp "FS::svc_acct::radius depriciated, use radius_reply";
840 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
841 reply attributes of this record.
843 Note that this is now the preferred method for reading RADIUS attributes -
844 accessing the columns directly is discouraged, as the column names are
845 expected to change in the future.
854 my($column, $attrib) = ($1, $2);
855 #$attrib =~ s/_/\-/g;
856 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
857 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
858 if ( $self->slipip && $self->slipip ne '0e0' ) {
859 $reply{'Framed-IP-Address'} = $self->slipip;
866 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
867 check attributes of this record.
869 Note that this is now the preferred method for reading RADIUS attributes -
870 accessing the columns directly is discouraged, as the column names are
871 expected to change in the future.
877 my $password = $self->_password;
878 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
879 ( $pw_attrib => $password,
882 my($column, $attrib) = ($1, $2);
883 #$attrib =~ s/_/\-/g;
884 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
885 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
891 Returns the domain associated with this account.
897 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
898 my $svc_domain = $self->svc_domain
899 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
905 Returns the FS::svc_domain record for this account's domain (see
914 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
919 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
923 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
928 Returns an email address associated with the account.
934 $self->username. '@'. $self->domain;
937 =item seconds_since TIMESTAMP
939 Returns the number of seconds this account has been online since TIMESTAMP,
940 according to the session monitor (see L<FS::Session>).
942 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
943 L<Time::Local> and L<Date::Parse> for conversion functions.
947 #note: POD here, implementation in FS::cust_svc
950 $self->cust_svc->seconds_since(@_);
953 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
955 Returns the numbers of seconds this account has been online between
956 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
957 external SQL radacct table, specified via sqlradius export. Sessions which
958 started in the specified range but are still open are counted from session
959 start to the end of the range (unless they are over 1 day old, in which case
960 they are presumed missing their stop record and not counted). Also, sessions
961 which end in therange but started earlier are counted from the start of the
962 range to session end. Finally, sessions which start before the range but end
963 after are counted for the entire range.
965 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
966 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
971 #note: POD here, implementation in FS::cust_svc
972 sub seconds_since_sqlradacct {
974 $self->cust_svc->seconds_since_sqlradacct(@_);
977 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
979 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
980 in this package for sessions ending between TIMESTAMP_START (inclusive) and
981 TIMESTAMP_END (exclusive).
983 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
984 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
989 #note: POD here, implementation in FS::cust_svc
990 sub attribute_since_sqlradacct {
992 $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<FS::svc_acct_pop>,
1202 schema.html from the base documentation.