4 use vars qw( @ISA $DEBUG $me $noexport_hack $conf
5 $dir_prefix @shells $usernamemin
6 $usernamemax $passwordmin $passwordmax
7 $username_ampersand $username_letter $username_letterfirst
8 $username_noperiod $username_nounderscore $username_nodash
11 $welcome_template $welcome_from $welcome_subject $welcome_mimetype
18 use FS::UID qw( datasrc );
20 use FS::Record qw( qsearch qsearchs fields dbh );
27 use FS::cust_main_invoice;
31 use FS::radius_usergroup;
34 use FS::Msgcat qw(gettext);
36 @ISA = qw( FS::svc_Common );
39 $me = '[FS::svc_acct]';
41 #ask FS::UID to run this stuff for us later
42 $FS::UID::callback{'FS::svc_acct'} = sub {
44 $dir_prefix = $conf->config('home');
45 @shells = $conf->config('shells');
46 $usernamemin = $conf->config('usernamemin') || 2;
47 $usernamemax = $conf->config('usernamemax');
48 $passwordmin = $conf->config('passwordmin') || 6;
49 $passwordmax = $conf->config('passwordmax') || 8;
50 $username_letter = $conf->exists('username-letter');
51 $username_letterfirst = $conf->exists('username-letterfirst');
52 $username_noperiod = $conf->exists('username-noperiod');
53 $username_nounderscore = $conf->exists('username-nounderscore');
54 $username_nodash = $conf->exists('username-nodash');
55 $username_uppercase = $conf->exists('username-uppercase');
56 $username_ampersand = $conf->exists('username-ampersand');
57 $mydomain = $conf->config('domain');
58 $dirhash = $conf->config('dirhash') || 0;
59 if ( $conf->exists('welcome_email') ) {
60 $welcome_template = new Text::Template (
62 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
63 ) or warn "can't create welcome email template: $Text::Template::ERROR";
64 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
65 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
66 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
68 $welcome_template = '';
70 $smtpmachine = $conf->config('smtpmachine');
71 $radius_password = $conf->config('radius-password') || 'Password';
74 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
75 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
79 my ( $hashref, $cache ) = @_;
80 if ( $hashref->{'svc_acct_svcnum'} ) {
81 $self->{'_domsvc'} = FS::svc_domain->new( {
82 'svcnum' => $hashref->{'domsvc'},
83 'domain' => $hashref->{'svc_acct_domain'},
84 'catchall' => $hashref->{'svc_acct_catchall'},
91 FS::svc_acct - Object methods for svc_acct records
97 $record = new FS::svc_acct \%hash;
98 $record = new FS::svc_acct { 'column' => 'value' };
100 $error = $record->insert;
102 $error = $new_record->replace($old_record);
104 $error = $record->delete;
106 $error = $record->check;
108 $error = $record->suspend;
110 $error = $record->unsuspend;
112 $error = $record->cancel;
114 %hash = $record->radius;
116 %hash = $record->radius_reply;
118 %hash = $record->radius_check;
120 $domain = $record->domain;
122 $svc_domain = $record->svc_domain;
124 $email = $record->email;
126 $seconds_since = $record->seconds_since($timestamp);
130 An FS::svc_acct object represents an account. FS::svc_acct inherits from
131 FS::svc_Common. The following fields are currently supported:
135 =item svcnum - primary key (assigned automatcially for new accounts)
139 =item _password - generated if blank
141 =item sec_phrase - security phrase
143 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
151 =item dir - set automatically if blank (and uid is not)
155 =item quota - (unimplementd)
157 =item slipip - IP address
161 =item domsvc - svcnum from svc_domain
163 =item radius_I<Radius_Attribute> - I<Radius-Attribute>
173 Creates a new account. To add the account to the database, see L<"insert">.
177 sub table { 'svc_acct'; }
181 Adds this account to the database. If there is an error, returns the error,
182 otherwise returns false.
184 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
185 defined. An FS::cust_svc record will be created and inserted.
187 The additional field I<usergroup> can optionally be defined; if so it should
188 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
189 sqlradius export only)
191 (TODOC: L<FS::queue> and L<freeside-queued>)
193 (TODOC: new exports! $noexport_hack)
201 local $SIG{HUP} = 'IGNORE';
202 local $SIG{INT} = 'IGNORE';
203 local $SIG{QUIT} = 'IGNORE';
204 local $SIG{TERM} = 'IGNORE';
205 local $SIG{TSTP} = 'IGNORE';
206 local $SIG{PIPE} = 'IGNORE';
208 my $oldAutoCommit = $FS::UID::AutoCommit;
209 local $FS::UID::AutoCommit = 0;
212 $error = $self->check;
213 return $error if $error;
215 #no, duplicate checking just got a whole lot more complicated
216 #(perhaps keep this check with a config option to turn on?)
218 #return gettext('username_in_use'). ": ". $self->username
219 # if qsearchs( 'svc_acct', { 'username' => $self->username,
220 # 'domsvc' => $self->domsvc,
223 if ( $self->svcnum ) {
224 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
225 unless ( $cust_svc ) {
226 $dbh->rollback if $oldAutoCommit;
227 return "no cust_svc record found for svcnum ". $self->svcnum;
229 $self->pkgnum($cust_svc->pkgnum);
230 $self->svcpart($cust_svc->svcpart);
233 #new duplicate username checking
235 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
236 unless ( $part_svc ) {
237 $dbh->rollback if $oldAutoCommit;
238 return 'unknown svcpart '. $self->svcpart;
241 my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
242 my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
243 'domsvc' => $self->domsvc } );
245 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
246 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
247 @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
252 if ( @dup_user || @dup_userdomain || @dup_uid ) {
253 my $exports = FS::part_export::export_info('svc_acct');
254 my %conflict_user_svcpart;
255 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
257 foreach my $part_export ( $part_svc->part_export ) {
259 #this will catch to the same exact export
260 my @svcparts = map { $_->svcpart }
261 qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
263 #this will catch to exports w/same exporthost+type ???
264 #my @other_part_export = qsearch('part_export', {
265 # 'machine' => $part_export->machine,
266 # 'exporttype' => $part_export->exporttype,
268 #foreach my $other_part_export ( @other_part_export ) {
269 # push @svcparts, map { $_->svcpart }
270 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
273 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
274 #silly kludge to avoid uninitialized value errors
275 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
276 ? $exports->{$part_export->exporttype}{'nodomain'}
278 if ( $nodomain =~ /^Y/i ) {
279 $conflict_user_svcpart{$_} = $part_export->exportnum
282 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
287 foreach my $dup_user ( @dup_user ) {
288 my $dup_svcpart = $dup_user->cust_svc->svcpart;
289 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
290 $dbh->rollback if $oldAutoCommit;
291 return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
292 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
296 foreach my $dup_userdomain ( @dup_userdomain ) {
297 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
298 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
299 $dbh->rollback if $oldAutoCommit;
300 return "duplicate username\@domain: conflicts with svcnum ".
301 $dup_userdomain->svcnum. " via exportnum ".
302 $conflict_userdomain_svcpart{$dup_svcpart};
306 foreach my $dup_uid ( @dup_uid ) {
307 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
308 if ( exists($conflict_user_svcpart{$dup_svcpart})
309 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
310 $dbh->rollback if $oldAutoCommit;
311 return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
312 "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
313 || $conflict_userdomain_svcpart{$dup_svcpart};
319 #see? i told you it was more complicated
322 $error = $self->SUPER::insert(\@jobnums);
324 $dbh->rollback if $oldAutoCommit;
328 if ( $self->usergroup ) {
329 foreach my $groupname ( @{$self->usergroup} ) {
330 my $radius_usergroup = new FS::radius_usergroup ( {
331 svcnum => $self->svcnum,
332 groupname => $groupname,
334 my $error = $radius_usergroup->insert;
336 $dbh->rollback if $oldAutoCommit;
342 #false laziness with sub replace (and cust_main)
343 my $queue = new FS::queue {
344 'svcnum' => $self->svcnum,
345 'job' => 'FS::svc_acct::append_fuzzyfiles'
347 $error = $queue->insert($self->username);
349 $dbh->rollback if $oldAutoCommit;
350 return "queueing job (transaction rolled back): $error";
353 my $cust_pkg = $self->cust_svc->cust_pkg;
356 my $cust_main = $cust_pkg->cust_main;
358 if ( $conf->exists('emailinvoiceauto') ) {
359 my @invoicing_list = $cust_main->invoicing_list;
360 push @invoicing_list, $self->email;
361 $cust_main->invoicing_list(\@invoicing_list);
366 if ( $welcome_template && $cust_pkg ) {
367 my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
369 my $wqueue = new FS::queue {
370 'svcnum' => $self->svcnum,
371 'job' => 'FS::svc_acct::send_email'
373 warn "attempting to queue email to $to";
374 my $error = $wqueue->insert(
376 'from' => $welcome_from,
377 'subject' => $welcome_subject,
378 'mimetype' => $welcome_mimetype,
379 'body' => $welcome_template->fill_in( HASH => {
380 'custnum' => $self->custnum,
381 'username' => $self->username,
382 'password' => $self->_password,
383 'first' => $cust_main->first,
384 'last' => $cust_main->getfield('last'),
385 'pkg' => $cust_pkg->part_pkg->pkg,
389 $dbh->rollback if $oldAutoCommit;
390 return "queuing welcome email: $error";
393 foreach my $jobnum ( @jobnums ) {
394 my $error = $wqueue->depend_insert($jobnum);
396 $dbh->rollback if $oldAutoCommit;
397 return "queuing welcome email job dependancy: $error";
407 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
413 Deletes this account from the database. If there is an error, returns the
414 error, otherwise returns false.
416 The corresponding FS::cust_svc record will be deleted as well.
418 (TODOC: new exports! $noexport_hack)
425 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
426 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
427 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
430 return "Can't delete an account which is a (svc_forward) source!"
431 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
433 return "Can't delete an account which is a (svc_forward) destination!"
434 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
436 return "Can't delete an account with (svc_www) web service!"
437 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
439 # what about records in session ? (they should refer to history table)
441 local $SIG{HUP} = 'IGNORE';
442 local $SIG{INT} = 'IGNORE';
443 local $SIG{QUIT} = 'IGNORE';
444 local $SIG{TERM} = 'IGNORE';
445 local $SIG{TSTP} = 'IGNORE';
446 local $SIG{PIPE} = 'IGNORE';
448 my $oldAutoCommit = $FS::UID::AutoCommit;
449 local $FS::UID::AutoCommit = 0;
452 foreach my $cust_main_invoice (
453 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
455 unless ( defined($cust_main_invoice) ) {
456 warn "WARNING: something's wrong with qsearch";
459 my %hash = $cust_main_invoice->hash;
460 $hash{'dest'} = $self->email;
461 my $new = new FS::cust_main_invoice \%hash;
462 my $error = $new->replace($cust_main_invoice);
464 $dbh->rollback if $oldAutoCommit;
469 foreach my $svc_domain (
470 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
472 my %hash = new FS::svc_domain->hash;
473 $hash{'catchall'} = '';
474 my $new = new FS::svc_domain \%hash;
475 my $error = $new->replace($svc_domain);
477 $dbh->rollback if $oldAutoCommit;
482 foreach my $radius_usergroup (
483 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
485 my $error = $radius_usergroup->delete;
487 $dbh->rollback if $oldAutoCommit;
492 my $error = $self->SUPER::delete;
494 $dbh->rollback if $oldAutoCommit;
498 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
502 =item replace OLD_RECORD
504 Replaces OLD_RECORD with this one in the database. If there is an error,
505 returns the error, otherwise returns false.
507 The additional field I<usergroup> can optionally be defined; if so it should
508 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
509 sqlradius export only)
514 my ( $new, $old ) = ( shift, shift );
516 warn "$me replacing $old with $new\n" if $DEBUG;
518 return "Username in use"
519 if $old->username ne $new->username &&
520 qsearchs( 'svc_acct', { 'username' => $new->username,
521 'domsvc' => $new->domsvc,
524 #no warnings 'numeric'; #alas, a 5.006-ism
526 return "Can't change uid!" if $old->uid != $new->uid;
529 #change homdir when we change username
530 $new->setfield('dir', '') if $old->username ne $new->username;
532 local $SIG{HUP} = 'IGNORE';
533 local $SIG{INT} = 'IGNORE';
534 local $SIG{QUIT} = 'IGNORE';
535 local $SIG{TERM} = 'IGNORE';
536 local $SIG{TSTP} = 'IGNORE';
537 local $SIG{PIPE} = 'IGNORE';
539 my $oldAutoCommit = $FS::UID::AutoCommit;
540 local $FS::UID::AutoCommit = 0;
543 # redundant, but so $new->usergroup gets set
544 $error = $new->check;
545 return $error if $error;
547 $old->usergroup( [ $old->radius_groups ] );
548 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
549 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
550 if ( $new->usergroup ) {
551 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
552 my @newgroups = @{$new->usergroup};
553 foreach my $oldgroup ( @{$old->usergroup} ) {
554 if ( grep { $oldgroup eq $_ } @newgroups ) {
555 @newgroups = grep { $oldgroup ne $_ } @newgroups;
558 my $radius_usergroup = qsearchs('radius_usergroup', {
559 svcnum => $old->svcnum,
560 groupname => $oldgroup,
562 my $error = $radius_usergroup->delete;
564 $dbh->rollback if $oldAutoCommit;
565 return "error deleting radius_usergroup $oldgroup: $error";
569 foreach my $newgroup ( @newgroups ) {
570 my $radius_usergroup = new FS::radius_usergroup ( {
571 svcnum => $new->svcnum,
572 groupname => $newgroup,
574 my $error = $radius_usergroup->insert;
576 $dbh->rollback if $oldAutoCommit;
577 return "error adding radius_usergroup $newgroup: $error";
583 $error = $new->SUPER::replace($old);
585 $dbh->rollback if $oldAutoCommit;
586 return $error if $error;
589 if ( $new->username ne $old->username ) {
590 #false laziness with sub insert (and cust_main)
591 my $queue = new FS::queue {
592 'svcnum' => $new->svcnum,
593 'job' => 'FS::svc_acct::append_fuzzyfiles'
595 $error = $queue->insert($new->username);
597 $dbh->rollback if $oldAutoCommit;
598 return "queueing job (transaction rolled back): $error";
602 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
608 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
609 error, returns the error, otherwise returns false.
611 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
617 my %hash = $self->hash;
618 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
619 || $hash{_password} eq '*'
621 $hash{_password} = '*SUSPENDED* '.$hash{_password};
622 my $new = new FS::svc_acct ( \%hash );
623 my $error = $new->replace($self);
624 return $error if $error;
627 $self->SUPER::suspend;
632 Unsuspends this account by removing *SUSPENDED* from the password. If there is
633 an error, returns the error, otherwise returns false.
635 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
641 my %hash = $self->hash;
642 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
643 $hash{_password} = $1;
644 my $new = new FS::svc_acct ( \%hash );
645 my $error = $new->replace($self);
646 return $error if $error;
649 $self->SUPER::unsuspend;
654 Just returns false (no error) for now.
656 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
660 Checks all fields to make sure this is a valid service. If there is an error,
661 returns the error, otherwise returns false. Called by the insert and replace
664 Sets any fixed values; see L<FS::part_svc>.
671 my($recref) = $self->hashref;
673 my $x = $self->setfixed;
674 return $x unless ref($x);
677 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
679 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
682 my $error = $self->ut_numbern('svcnum')
683 || $self->ut_number('domsvc')
684 || $self->ut_textn('sec_phrase')
686 return $error if $error;
688 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
689 if ( $username_uppercase ) {
690 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
691 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
692 $recref->{username} = $1;
694 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
695 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
696 $recref->{username} = $1;
699 if ( $username_letterfirst ) {
700 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
701 } elsif ( $username_letter ) {
702 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
704 if ( $username_noperiod ) {
705 $recref->{username} =~ /\./ and return gettext('illegal_username');
707 if ( $username_nounderscore ) {
708 $recref->{username} =~ /_/ and return gettext('illegal_username');
710 if ( $username_nodash ) {
711 $recref->{username} =~ /\-/ and return gettext('illegal_username');
713 unless ( $username_ampersand ) {
714 $recref->{username} =~ /\&/ and return gettext('illegal_username');
717 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
718 $recref->{popnum} = $1;
719 return "Unknown popnum" unless
720 ! $recref->{popnum} ||
721 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
723 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
725 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
726 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
728 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
729 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
730 #not all systems use gid=uid
731 #you can set a fixed gid in part_svc
733 return "Only root can have uid 0"
734 if $recref->{uid} == 0
735 && $recref->{username} ne 'root'
736 && $recref->{username} ne 'toor';
739 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
740 or return "Illegal directory: ". $recref->{dir};
742 return "Illegal directory"
743 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
744 return "Illegal directory"
745 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
746 unless ( $recref->{dir} ) {
747 $recref->{dir} = $dir_prefix . '/';
748 if ( $dirhash > 0 ) {
749 for my $h ( 1 .. $dirhash ) {
750 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
752 } elsif ( $dirhash < 0 ) {
753 for my $h ( reverse $dirhash .. -1 ) {
754 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
757 $recref->{dir} .= $recref->{username};
761 unless ( $recref->{username} eq 'sync' ) {
762 if ( grep $_ eq $recref->{shell}, @shells ) {
763 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
765 return "Illegal shell \`". $self->shell. "\'; ".
766 $conf->dir. "/shells contains: @shells";
769 $recref->{shell} = '/bin/sync';
773 $recref->{gid} ne '' ?
774 return "Can't have gid without uid" : ( $recref->{gid}='' );
775 $recref->{dir} ne '' ?
776 return "Can't have directory without uid" : ( $recref->{dir}='' );
777 $recref->{shell} ne '' ?
778 return "Can't have shell without uid" : ( $recref->{shell}='' );
781 # $error = $self->ut_textn('finger');
782 # return $error if $error;
783 $self->getfield('finger') =~
784 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
785 or return "Illegal finger: ". $self->getfield('finger');
786 $self->setfield('finger', $1);
788 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
789 $recref->{quota} = $1;
791 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
792 unless ( $recref->{slipip} eq '0e0' ) {
793 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
794 or return "Illegal slipip". $self->slipip;
795 $recref->{slipip} = $1;
797 $recref->{slipip} = '0e0';
802 #arbitrary RADIUS stuff; allow ut_textn for now
803 foreach ( grep /^radius_/, fields('svc_acct') ) {
807 #generate a password if it is blank
808 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
809 unless ( $recref->{_password} );
811 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
812 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
813 $recref->{_password} = $1.$3;
814 #uncomment this to encrypt password immediately upon entry, or run
815 #bin/crypt_pw in cron to give new users a window during which their
816 #password is available to techs, for faxing, etc. (also be aware of
818 #$recref->{password} = $1.
819 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
821 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
822 $recref->{_password} = $1.$3;
823 } elsif ( $recref->{_password} eq '*' ) {
824 $recref->{_password} = '*';
825 } elsif ( $recref->{_password} eq '!!' ) {
826 $recref->{_password} = '!!';
828 #return "Illegal password";
829 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
830 FS::Msgcat::_gettext('illegal_password_characters').
831 ": ". $recref->{_password};
839 Depriciated, use radius_reply instead.
844 carp "FS::svc_acct::radius depriciated, use radius_reply";
850 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
851 reply attributes of this record.
853 Note that this is now the preferred method for reading RADIUS attributes -
854 accessing the columns directly is discouraged, as the column names are
855 expected to change in the future.
864 my($column, $attrib) = ($1, $2);
865 #$attrib =~ s/_/\-/g;
866 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
867 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
868 if ( $self->slipip && $self->slipip ne '0e0' ) {
869 $reply{'Framed-IP-Address'} = $self->slipip;
876 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
877 check attributes of this record.
879 Note that this is now the preferred method for reading RADIUS attributes -
880 accessing the columns directly is discouraged, as the column names are
881 expected to change in the future.
887 my $password = $self->_password;
888 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
889 ( $pw_attrib => $self->_password,
892 my($column, $attrib) = ($1, $2);
893 #$attrib =~ s/_/\-/g;
894 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
895 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
901 Returns the domain associated with this account.
907 if ( $self->domsvc ) {
908 #$self->svc_domain->domain;
909 my $svc_domain = $self->svc_domain
910 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
913 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
919 Returns the FS::svc_domain record for this account's domain (see
928 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
933 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
937 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
942 Returns an email address associated with the account.
948 $self->username. '@'. $self->domain;
951 =item seconds_since TIMESTAMP
953 Returns the number of seconds this account has been online since TIMESTAMP,
954 according to the session monitor (see L<FS::Session>).
956 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
957 L<Time::Local> and L<Date::Parse> for conversion functions.
961 #note: POD here, implementation in FS::cust_svc
964 $self->cust_svc->seconds_since(@_);
967 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
969 Returns the numbers of seconds this account has been online between
970 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
971 external SQL radacct table, specified via sqlradius export. Sessions which
972 started in the specified range but are still open are counted from session
973 start to the end of the range (unless they are over 1 day old, in which case
974 they are presumed missing their stop record and not counted). Also, sessions
975 which end in the range but started earlier are counted from the start of the
976 range to session end. Finally, sessions which start before the range but end
977 after are counted for the entire range.
979 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
980 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
985 #note: POD here, implementation in FS::cust_svc
986 sub seconds_since_sqlradacct {
988 $self->cust_svc->seconds_since_sqlradacct(@_);
991 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
993 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
994 in this package for sessions ending between TIMESTAMP_START (inclusive) and
995 TIMESTAMP_END (exclusive).
997 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
998 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1003 #note: POD here, implementation in FS::cust_svc
1004 sub attribute_since_sqlradacct {
1006 $self->cust_svc->attribute_since_sqlradacct(@_);
1012 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1018 if ( $self->usergroup ) {
1019 #when provisioning records, export callback runs in svc_Common.pm before
1020 #radius_usergroup records can be inserted...
1021 @{$self->usergroup};
1023 map { $_->groupname }
1024 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1042 use Mail::Internet 1.44;
1045 $opt{mimetype} ||= 'text/plain';
1046 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1048 $ENV{MAILADDRESS} = $opt{from};
1049 my $header = new Mail::Header ( [
1052 "Sender: $opt{from}",
1053 "Reply-To: $opt{from}",
1054 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1055 "Subject: $opt{subject}",
1056 "Content-Type: $opt{mimetype}",
1058 my $message = new Mail::Internet (
1059 'Header' => $header,
1060 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1063 $message->smtpsend( Host => $smtpmachine )
1064 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1065 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1068 =item check_and_rebuild_fuzzyfiles
1072 sub check_and_rebuild_fuzzyfiles {
1073 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1074 -e "$dir/svc_acct.username"
1075 or &rebuild_fuzzyfiles;
1078 =item rebuild_fuzzyfiles
1082 sub rebuild_fuzzyfiles {
1084 use Fcntl qw(:flock);
1086 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1090 open(USERNAMELOCK,">>$dir/svc_acct.username")
1091 or die "can't open $dir/svc_acct.username: $!";
1092 flock(USERNAMELOCK,LOCK_EX)
1093 or die "can't lock $dir/svc_acct.username: $!";
1095 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1097 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1098 or die "can't open $dir/svc_acct.username.tmp: $!";
1099 print USERNAMECACHE join("\n", @all_username), "\n";
1100 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1102 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1112 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1113 open(USERNAMECACHE,"<$dir/svc_acct.username")
1114 or die "can't open $dir/svc_acct.username: $!";
1115 my @array = map { chomp; $_; } <USERNAMECACHE>;
1116 close USERNAMECACHE;
1120 =item append_fuzzyfiles USERNAME
1124 sub append_fuzzyfiles {
1125 my $username = shift;
1127 &check_and_rebuild_fuzzyfiles;
1129 use Fcntl qw(:flock);
1131 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1133 open(USERNAME,">>$dir/svc_acct.username")
1134 or die "can't open $dir/svc_acct.username: $!";
1135 flock(USERNAME,LOCK_EX)
1136 or die "can't lock $dir/svc_acct.username: $!";
1138 print USERNAME "$username\n";
1140 flock(USERNAME,LOCK_UN)
1141 or die "can't unlock $dir/svc_acct.username: $!";
1149 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1153 sub radius_usergroup_selector {
1154 my $sel_groups = shift;
1155 my %sel_groups = map { $_=>1 } @$sel_groups;
1157 my $selectname = shift || 'radius_usergroup';
1160 my $sth = $dbh->prepare(
1161 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1162 ) or die $dbh->errstr;
1163 $sth->execute() or die $sth->errstr;
1164 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1168 function ${selectname}_doadd(object) {
1169 var myvalue = object.${selectname}_add.value;
1170 var optionName = new Option(myvalue,myvalue,false,true);
1171 var length = object.$selectname.length;
1172 object.$selectname.options[length] = optionName;
1173 object.${selectname}_add.value = "";
1176 <SELECT MULTIPLE NAME="$selectname">
1179 foreach my $group ( @all_groups ) {
1181 if ( $sel_groups{$group} ) {
1182 $html .= ' SELECTED';
1183 $sel_groups{$group} = 0;
1185 $html .= ">$group</OPTION>\n";
1187 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1188 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1190 $html .= '</SELECT>';
1192 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1193 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1202 The $recref stuff in sub check should be cleaned up.
1204 The suspend, unsuspend and cancel methods update the database, but not the
1205 current object. This is probably a bug as it's unexpected and
1208 radius_usergroup_selector? putting web ui components in here? they should
1209 probably live somewhere else...
1213 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1214 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1215 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1216 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1217 schema.html from the base documentation.