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 $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!)
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 my $error = $wqueue->insert(
375 'from' => $welcome_from,
376 'subject' => $welcome_subject,
377 'mimetype' => $welcome_mimetype,
378 'body' => $welcome_template->fill_in( HASH => {
379 'custnum' => $self->custnum,
380 'username' => $self->username,
381 'password' => $self->_password,
382 'first' => $cust_main->first,
383 'last' => $cust_main->getfield('last'),
384 'pkg' => $cust_pkg->part_pkg->pkg,
388 $dbh->rollback if $oldAutoCommit;
389 return "error queuing welcome email: $error";
392 foreach my $jobnum ( @jobnums ) {
393 my $error = $wqueue->depend_insert($jobnum);
395 $dbh->rollback if $oldAutoCommit;
396 return "error queuing welcome email job dependancy: $error";
406 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
412 Deletes this account from the database. If there is an error, returns the
413 error, otherwise returns false.
415 The corresponding FS::cust_svc record will be deleted as well.
417 (TODOC: new exports!)
424 if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) {
425 return "Can't delete an account which has (svc_acct_sm) mail aliases!"
426 if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } );
429 return "Can't delete an account which is a (svc_forward) source!"
430 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
432 return "Can't delete an account which is a (svc_forward) destination!"
433 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
435 return "Can't delete an account with (svc_www) web service!"
436 if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
438 # what about records in session ? (they should refer to history table)
440 local $SIG{HUP} = 'IGNORE';
441 local $SIG{INT} = 'IGNORE';
442 local $SIG{QUIT} = 'IGNORE';
443 local $SIG{TERM} = 'IGNORE';
444 local $SIG{TSTP} = 'IGNORE';
445 local $SIG{PIPE} = 'IGNORE';
447 my $oldAutoCommit = $FS::UID::AutoCommit;
448 local $FS::UID::AutoCommit = 0;
451 foreach my $cust_main_invoice (
452 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
454 unless ( defined($cust_main_invoice) ) {
455 warn "WARNING: something's wrong with qsearch";
458 my %hash = $cust_main_invoice->hash;
459 $hash{'dest'} = $self->email;
460 my $new = new FS::cust_main_invoice \%hash;
461 my $error = $new->replace($cust_main_invoice);
463 $dbh->rollback if $oldAutoCommit;
468 foreach my $svc_domain (
469 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
471 my %hash = new FS::svc_domain->hash;
472 $hash{'catchall'} = '';
473 my $new = new FS::svc_domain \%hash;
474 my $error = $new->replace($svc_domain);
476 $dbh->rollback if $oldAutoCommit;
481 foreach my $radius_usergroup (
482 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
484 my $error = $radius_usergroup->delete;
486 $dbh->rollback if $oldAutoCommit;
491 my $error = $self->SUPER::delete;
493 $dbh->rollback if $oldAutoCommit;
497 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
501 =item replace OLD_RECORD
503 Replaces OLD_RECORD with this one in the database. If there is an error,
504 returns the error, otherwise returns false.
506 The additional field I<usergroup> can optionally be defined; if so it should
507 contain an arrayref of group names. See L<FS::radius_usergroup>. (used in
508 sqlradius export only)
513 my ( $new, $old ) = ( shift, shift );
515 warn "$me replacing $old with $new\n" if $DEBUG;
517 return "Username in use"
518 if $old->username ne $new->username &&
519 qsearchs( 'svc_acct', { 'username' => $new->username,
520 'domsvc' => $new->domsvc,
523 #no warnings 'numeric'; #alas, a 5.006-ism
525 return "Can't change uid!" if $old->uid != $new->uid;
528 #change homdir when we change username
529 $new->setfield('dir', '') if $old->username ne $new->username;
531 local $SIG{HUP} = 'IGNORE';
532 local $SIG{INT} = 'IGNORE';
533 local $SIG{QUIT} = 'IGNORE';
534 local $SIG{TERM} = 'IGNORE';
535 local $SIG{TSTP} = 'IGNORE';
536 local $SIG{PIPE} = 'IGNORE';
538 my $oldAutoCommit = $FS::UID::AutoCommit;
539 local $FS::UID::AutoCommit = 0;
542 # redundant, but so $new->usergroup gets set
543 $error = $new->check;
544 return $error if $error;
546 $old->usergroup( [ $old->radius_groups ] );
547 warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
548 warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
549 if ( $new->usergroup ) {
550 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
551 my @newgroups = @{$new->usergroup};
552 foreach my $oldgroup ( @{$old->usergroup} ) {
553 if ( grep { $oldgroup eq $_ } @newgroups ) {
554 @newgroups = grep { $oldgroup ne $_ } @newgroups;
557 my $radius_usergroup = qsearchs('radius_usergroup', {
558 svcnum => $old->svcnum,
559 groupname => $oldgroup,
561 my $error = $radius_usergroup->delete;
563 $dbh->rollback if $oldAutoCommit;
564 return "error deleting radius_usergroup $oldgroup: $error";
568 foreach my $newgroup ( @newgroups ) {
569 my $radius_usergroup = new FS::radius_usergroup ( {
570 svcnum => $new->svcnum,
571 groupname => $newgroup,
573 my $error = $radius_usergroup->insert;
575 $dbh->rollback if $oldAutoCommit;
576 return "error adding radius_usergroup $newgroup: $error";
582 $error = $new->SUPER::replace($old);
584 $dbh->rollback if $oldAutoCommit;
585 return $error if $error;
588 if ( $new->username ne $old->username ) {
589 #false laziness with sub insert (and cust_main)
590 my $queue = new FS::queue {
591 'svcnum' => $new->svcnum,
592 'job' => 'FS::svc_acct::append_fuzzyfiles'
594 $error = $queue->insert($new->username);
596 $dbh->rollback if $oldAutoCommit;
597 return "queueing job (transaction rolled back): $error";
601 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
607 Suspends this account by prefixing *SUSPENDED* to the password. If there is an
608 error, returns the error, otherwise returns false.
610 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
616 my %hash = $self->hash;
617 unless ( $hash{_password} =~ /^\*SUSPENDED\* /
618 || $hash{_password} eq '*'
620 $hash{_password} = '*SUSPENDED* '.$hash{_password};
621 my $new = new FS::svc_acct ( \%hash );
622 my $error = $new->replace($self);
623 return $error if $error;
626 $self->SUPER::suspend;
631 Unsuspends this account by removing *SUSPENDED* from the password. If there is
632 an error, returns the error, otherwise returns false.
634 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
640 my %hash = $self->hash;
641 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
642 $hash{_password} = $1;
643 my $new = new FS::svc_acct ( \%hash );
644 my $error = $new->replace($self);
645 return $error if $error;
648 $self->SUPER::unsuspend;
653 Just returns false (no error) for now.
655 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
659 Checks all fields to make sure this is a valid service. If there is an error,
660 returns the error, otherwise returns false. Called by the insert and replace
663 Sets any fixed values; see L<FS::part_svc>.
670 my($recref) = $self->hashref;
672 my $x = $self->setfixed;
673 return $x unless ref($x);
676 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
678 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
681 my $error = $self->ut_numbern('svcnum')
682 || $self->ut_number('domsvc')
683 || $self->ut_textn('sec_phrase')
685 return $error if $error;
687 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
688 if ( $username_uppercase ) {
689 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
690 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
691 $recref->{username} = $1;
693 $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
694 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
695 $recref->{username} = $1;
698 if ( $username_letterfirst ) {
699 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
700 } elsif ( $username_letter ) {
701 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
703 if ( $username_noperiod ) {
704 $recref->{username} =~ /\./ and return gettext('illegal_username');
706 if ( $username_nounderscore ) {
707 $recref->{username} =~ /_/ and return gettext('illegal_username');
709 if ( $username_nodash ) {
710 $recref->{username} =~ /\-/ and return gettext('illegal_username');
712 unless ( $username_ampersand ) {
713 $recref->{username} =~ /\&/ and return gettext('illegal_username');
716 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
717 $recref->{popnum} = $1;
718 return "Unknown popnum" unless
719 ! $recref->{popnum} ||
720 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
722 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
724 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
725 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
727 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
728 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
729 #not all systems use gid=uid
730 #you can set a fixed gid in part_svc
732 return "Only root can have uid 0"
733 if $recref->{uid} == 0
734 && $recref->{username} ne 'root'
735 && $recref->{username} ne 'toor';
738 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
739 or return "Illegal directory: ". $recref->{dir};
741 return "Illegal directory"
742 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
743 return "Illegal directory"
744 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
745 unless ( $recref->{dir} ) {
746 $recref->{dir} = $dir_prefix . '/';
747 if ( $dirhash > 0 ) {
748 for my $h ( 1 .. $dirhash ) {
749 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
751 } elsif ( $dirhash < 0 ) {
752 for my $h ( reverse $dirhash .. -1 ) {
753 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
756 $recref->{dir} .= $recref->{username};
760 unless ( $recref->{username} eq 'sync' ) {
761 if ( grep $_ eq $recref->{shell}, @shells ) {
762 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
764 return "Illegal shell \`". $self->shell. "\'; ".
765 $conf->dir. "/shells contains: @shells";
768 $recref->{shell} = '/bin/sync';
772 $recref->{gid} ne '' ?
773 return "Can't have gid without uid" : ( $recref->{gid}='' );
774 $recref->{dir} ne '' ?
775 return "Can't have directory without uid" : ( $recref->{dir}='' );
776 $recref->{shell} ne '' ?
777 return "Can't have shell without uid" : ( $recref->{shell}='' );
780 # $error = $self->ut_textn('finger');
781 # return $error if $error;
782 $self->getfield('finger') =~
783 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
784 or return "Illegal finger: ". $self->getfield('finger');
785 $self->setfield('finger', $1);
787 $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
788 $recref->{quota} = $1;
790 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
791 unless ( $recref->{slipip} eq '0e0' ) {
792 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
793 or return "Illegal slipip". $self->slipip;
794 $recref->{slipip} = $1;
796 $recref->{slipip} = '0e0';
801 #arbitrary RADIUS stuff; allow ut_textn for now
802 foreach ( grep /^radius_/, fields('svc_acct') ) {
806 #generate a password if it is blank
807 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
808 unless ( $recref->{_password} );
810 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
811 if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
812 $recref->{_password} = $1.$3;
813 #uncomment this to encrypt password immediately upon entry, or run
814 #bin/crypt_pw in cron to give new users a window during which their
815 #password is available to techs, for faxing, etc. (also be aware of
817 #$recref->{password} = $1.
818 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
820 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
821 $recref->{_password} = $1.$3;
822 } elsif ( $recref->{_password} eq '*' ) {
823 $recref->{_password} = '*';
824 } elsif ( $recref->{_password} eq '!!' ) {
825 $recref->{_password} = '!!';
827 #return "Illegal password";
828 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
829 FS::Msgcat::_gettext('illegal_password_characters').
830 ": ". $recref->{_password};
838 Depriciated, use radius_reply instead.
843 carp "FS::svc_acct::radius depriciated, use radius_reply";
849 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
850 reply attributes of this record.
852 Note that this is now the preferred method for reading RADIUS attributes -
853 accessing the columns directly is discouraged, as the column names are
854 expected to change in the future.
863 my($column, $attrib) = ($1, $2);
864 #$attrib =~ s/_/\-/g;
865 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
866 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
867 if ( $self->slipip && $self->slipip ne '0e0' ) {
868 $reply{'Framed-IP-Address'} = $self->slipip;
875 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
876 check attributes of this record.
878 Note that this is now the preferred method for reading RADIUS attributes -
879 accessing the columns directly is discouraged, as the column names are
880 expected to change in the future.
886 my $password = $self->_password;
887 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
888 ( $pw_attrib => $self->_password,
891 my($column, $attrib) = ($1, $2);
892 #$attrib =~ s/_/\-/g;
893 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
894 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
900 Returns the domain associated with this account.
906 if ( $self->domsvc ) {
907 #$self->svc_domain->domain;
908 my $svc_domain = $self->svc_domain
909 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
912 $mydomain or die "svc_acct.domsvc is null and no legacy domain config file";
918 Returns the FS::svc_domain record for this account's domain (see
927 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
932 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
936 qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
941 Returns an email address associated with the account.
947 $self->username. '@'. $self->domain;
950 =item seconds_since TIMESTAMP
952 Returns the number of seconds this account has been online since TIMESTAMP,
953 according to the session monitor (see L<FS::Session>).
955 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
956 L<Time::Local> and L<Date::Parse> for conversion functions.
960 #note: POD here, implementation in FS::cust_svc
963 $self->cust_svc->seconds_since(@_);
966 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
968 Returns the numbers of seconds this account has been online between
969 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
970 external SQL radacct table, specified via sqlradius export. Sessions which
971 started in the specified range but are still open are counted from session
972 start to the end of the range (unless they are over 1 day old, in which case
973 they are presumed missing their stop record and not counted). Also, sessions
974 which end in the range but started earlier are counted from the start of the
975 range to session end. Finally, sessions which start before the range but end
976 after are counted for the entire range.
978 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
979 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
984 #note: POD here, implementation in FS::cust_svc
985 sub seconds_since_sqlradacct {
987 $self->cust_svc->seconds_since_sqlradacct(@_);
990 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
992 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
993 in this package for sessions ending between TIMESTAMP_START (inclusive) and
994 TIMESTAMP_END (exclusive).
996 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
997 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1002 #note: POD here, implementation in FS::cust_svc
1003 sub attribute_since_sqlradacct {
1005 $self->cust_svc->attribute_since_sqlradacct(@_);
1011 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
1017 if ( $self->usergroup ) {
1018 #when provisioning records, export callback runs in svc_Common.pm before
1019 #radius_usergroup records can be inserted...
1020 @{$self->usergroup};
1022 map { $_->groupname }
1023 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
1041 use Mail::Internet 1.44;
1044 $opt{mimetype} ||= 'text/plain';
1045 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
1047 $ENV{MAILADDRESS} = $opt{from};
1048 my $header = new Mail::Header ( [
1051 "Sender: $opt{from}",
1052 "Reply-To: $opt{from}",
1053 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1054 "Subject: $opt{subject}",
1055 "Content-Type: $opt{mimetype}",
1057 my $message = new Mail::Internet (
1058 'Header' => $header,
1059 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
1062 $message->smtpsend( Host => $smtpmachine )
1063 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
1064 or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
1067 =item check_and_rebuild_fuzzyfiles
1071 sub check_and_rebuild_fuzzyfiles {
1072 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1073 -e "$dir/svc_acct.username"
1074 or &rebuild_fuzzyfiles;
1077 =item rebuild_fuzzyfiles
1081 sub rebuild_fuzzyfiles {
1083 use Fcntl qw(:flock);
1085 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1089 open(USERNAMELOCK,">>$dir/svc_acct.username")
1090 or die "can't open $dir/svc_acct.username: $!";
1091 flock(USERNAMELOCK,LOCK_EX)
1092 or die "can't lock $dir/svc_acct.username: $!";
1094 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
1096 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
1097 or die "can't open $dir/svc_acct.username.tmp: $!";
1098 print USERNAMECACHE join("\n", @all_username), "\n";
1099 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
1101 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
1111 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1112 open(USERNAMECACHE,"<$dir/svc_acct.username")
1113 or die "can't open $dir/svc_acct.username: $!";
1114 my @array = map { chomp; $_; } <USERNAMECACHE>;
1115 close USERNAMECACHE;
1119 =item append_fuzzyfiles USERNAME
1123 sub append_fuzzyfiles {
1124 my $username = shift;
1126 &check_and_rebuild_fuzzyfiles;
1128 use Fcntl qw(:flock);
1130 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1132 open(USERNAME,">>$dir/svc_acct.username")
1133 or die "can't open $dir/svc_acct.username: $!";
1134 flock(USERNAME,LOCK_EX)
1135 or die "can't lock $dir/svc_acct.username: $!";
1137 print USERNAME "$username\n";
1139 flock(USERNAME,LOCK_UN)
1140 or die "can't unlock $dir/svc_acct.username: $!";
1148 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
1152 sub radius_usergroup_selector {
1153 my $sel_groups = shift;
1154 my %sel_groups = map { $_=>1 } @$sel_groups;
1156 my $selectname = shift || 'radius_usergroup';
1159 my $sth = $dbh->prepare(
1160 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
1161 ) or die $dbh->errstr;
1162 $sth->execute() or die $sth->errstr;
1163 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
1167 function ${selectname}_doadd(object) {
1168 var myvalue = object.${selectname}_add.value;
1169 var optionName = new Option(myvalue,myvalue,false,true);
1170 var length = object.$selectname.length;
1171 object.$selectname.options[length] = optionName;
1172 object.${selectname}_add.value = "";
1175 <SELECT MULTIPLE NAME="$selectname">
1178 foreach my $group ( @all_groups ) {
1180 if ( $sel_groups{$group} ) {
1181 $html .= ' SELECTED';
1182 $sel_groups{$group} = 0;
1184 $html .= ">$group</OPTION>\n";
1186 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
1187 $html .= "<OPTION SELECTED>$group</OPTION>\n";
1189 $html .= '</SELECT>';
1191 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
1192 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
1201 The $recref stuff in sub check should be cleaned up.
1203 The suspend, unsuspend and cancel methods update the database, but not the
1204 current object. This is probably a bug as it's unexpected and
1207 radius_usergroup_selector? putting web ui components in here? they should
1208 probably live somewhere else...
1212 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
1213 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
1214 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
1215 L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>,
1216 schema.html from the base documentation.