4 use base qw( FS::svc_Domain_Mixin FS::svc_CGP_Mixin FS::svc_CGPRule_Mixin
6 use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
7 $dir_prefix @shells $usernamemin
8 $usernamemax $passwordmin $passwordmax
9 $username_ampersand $username_letter $username_letterfirst
10 $username_noperiod $username_nounderscore $username_nodash
11 $username_uppercase $username_percent $username_colon
12 $password_noampersand $password_noexclamation
13 $warning_template $warning_from $warning_subject $warning_mimetype
16 $radius_password $radius_ip
19 use Scalar::Util qw( blessed );
24 use Crypt::PasswdMD5 1.2;
25 use Digest::SHA1 'sha1_base64';
26 use Digest::MD5 'md5_base64';
29 use Authen::Passphrase;
30 use FS::UID qw( datasrc driver_name );
32 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
33 use FS::Msgcat qw(gettext);
34 use FS::UI::bytecount;
39 use FS::cust_main_invoice;
44 use FS::radius_usergroup;
52 $me = '[FS::svc_acct]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
57 $dir_prefix = $conf->config('home');
58 @shells = $conf->config('shells');
59 $usernamemin = $conf->config('usernamemin') || 2;
60 $usernamemax = $conf->config('usernamemax');
61 $passwordmin = $conf->config('passwordmin'); # || 6;
63 $passwordmin = ( defined($passwordmin) && $passwordmin =~ /\d+/ )
66 $passwordmax = $conf->config('passwordmax') || 8;
67 $username_letter = $conf->exists('username-letter');
68 $username_letterfirst = $conf->exists('username-letterfirst');
69 $username_noperiod = $conf->exists('username-noperiod');
70 $username_nounderscore = $conf->exists('username-nounderscore');
71 $username_nodash = $conf->exists('username-nodash');
72 $username_uppercase = $conf->exists('username-uppercase');
73 $username_ampersand = $conf->exists('username-ampersand');
74 $username_percent = $conf->exists('username-percent');
75 $username_colon = $conf->exists('username-colon');
76 $password_noampersand = $conf->exists('password-noexclamation');
77 $password_noexclamation = $conf->exists('password-noexclamation');
78 $dirhash = $conf->config('dirhash') || 0;
79 if ( $conf->exists('warning_email') ) {
80 $warning_template = new Text::Template (
82 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
83 ) or warn "can't create warning email template: $Text::Template::ERROR";
84 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
85 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
86 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
87 $warning_cc = $conf->config('warning_email-cc');
89 $warning_template = '';
91 $warning_subject = '';
92 $warning_mimetype = '';
95 $smtpmachine = $conf->config('smtpmachine');
96 $radius_password = $conf->config('radius-password') || 'Password';
97 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
98 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
102 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
103 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
107 my ( $hashref, $cache ) = @_;
108 if ( $hashref->{'svc_acct_svcnum'} ) {
109 $self->{'_domsvc'} = FS::svc_domain->new( {
110 'svcnum' => $hashref->{'domsvc'},
111 'domain' => $hashref->{'svc_acct_domain'},
112 'catchall' => $hashref->{'svc_acct_catchall'},
119 FS::svc_acct - Object methods for svc_acct records
125 $record = new FS::svc_acct \%hash;
126 $record = new FS::svc_acct { 'column' => 'value' };
128 $error = $record->insert;
130 $error = $new_record->replace($old_record);
132 $error = $record->delete;
134 $error = $record->check;
136 $error = $record->suspend;
138 $error = $record->unsuspend;
140 $error = $record->cancel;
142 %hash = $record->radius;
144 %hash = $record->radius_reply;
146 %hash = $record->radius_check;
148 $domain = $record->domain;
150 $svc_domain = $record->svc_domain;
152 $email = $record->email;
154 $seconds_since = $record->seconds_since($timestamp);
158 An FS::svc_acct object represents an account. FS::svc_acct inherits from
159 FS::svc_Common. The following fields are currently supported:
165 Primary key (assigned automatcially for new accounts)
173 =item _password_encoding
175 plain, crypt, ldap (or empty for autodetection)
183 Point of presence (see L<FS::svc_acct_pop>)
195 set automatically if blank (and uid is not)
215 svcnum from svc_domain
219 Optional svcnum from svc_pbx
221 =item radius_I<Radius_Attribute>
223 I<Radius-Attribute> (reply)
225 =item rc_I<Radius_Attribute>
227 I<Radius-Attribute> (check)
237 Creates a new account. To add the account to the database, see L<"insert">.
244 'longname_plural' => 'Access accounts and mailboxes',
245 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
246 'display_weight' => 10,
247 'cancel_weight' => 50,
249 'dir' => 'Home directory',
252 def_info => 'set to fixed and blank for no UIDs',
255 'slipip' => 'IP address',
256 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
258 label => 'Access number',
260 select_table => 'svc_acct_pop',
261 select_key => 'popnum',
262 select_label => 'city',
268 disable_default => 1,
272 'password_selfchange' => { label => 'Password modification',
275 'password_recover' => { label => 'Password recovery',
279 label => 'Quota', #Mail storage limit
281 disable_inventory => 1,
285 label => 'File storage limit',
287 disable_inventory => 1,
291 label => 'Number of files limit',
293 disable_inventory => 1,
297 label => 'File size limit',
299 disable_inventory => 1,
302 '_password' => 'Password',
305 def_info => 'when blank, defaults to UID',
310 def_info => 'set to blank for no shell tracking',
312 #select_list => [ $conf->config('shells') ],
313 select_list => [ $conf ? $conf->config('shells') : () ],
314 disable_inventory => 1,
317 'finger' => 'Real name', # (GECOS)',
321 select_table => 'svc_domain',
322 select_key => 'svcnum',
323 select_label => 'domain',
324 disable_inventory => 1,
326 'pbxsvc' => { label => 'PBX',
327 type => 'select-svc_pbx.html',
328 disable_inventory => 1,
329 disable_select => 1, #UI wonky, pry works otherwise
332 label => 'RADIUS groups',
333 type => 'radius_usergroup_selector',
334 disable_inventory => 1,
337 'seconds' => { label => 'Seconds',
338 label_sort => 'with Time Remaining',
340 disable_inventory => 1,
342 disable_part_svc_column => 1,
344 'upbytes' => { label => 'Upload',
346 disable_inventory => 1,
348 'format' => \&FS::UI::bytecount::display_bytecount,
349 'parse' => \&FS::UI::bytecount::parse_bytecount,
350 disable_part_svc_column => 1,
352 'downbytes' => { label => 'Download',
354 disable_inventory => 1,
356 'format' => \&FS::UI::bytecount::display_bytecount,
357 'parse' => \&FS::UI::bytecount::parse_bytecount,
358 disable_part_svc_column => 1,
360 'totalbytes'=> { label => 'Total up and download',
362 disable_inventory => 1,
364 'format' => \&FS::UI::bytecount::display_bytecount,
365 'parse' => \&FS::UI::bytecount::parse_bytecount,
366 disable_part_svc_column => 1,
368 'seconds_threshold' => { label => 'Seconds threshold',
370 disable_inventory => 1,
372 disable_part_svc_column => 1,
374 'upbytes_threshold' => { label => 'Upload threshold',
376 disable_inventory => 1,
378 'format' => \&FS::UI::bytecount::display_bytecount,
379 'parse' => \&FS::UI::bytecount::parse_bytecount,
380 disable_part_svc_column => 1,
382 'downbytes_threshold' => { label => 'Download threshold',
384 disable_inventory => 1,
386 'format' => \&FS::UI::bytecount::display_bytecount,
387 'parse' => \&FS::UI::bytecount::parse_bytecount,
388 disable_part_svc_column => 1,
390 'totalbytes_threshold'=> { label => 'Total up and download threshold',
392 disable_inventory => 1,
394 'format' => \&FS::UI::bytecount::display_bytecount,
395 'parse' => \&FS::UI::bytecount::parse_bytecount,
396 disable_part_svc_column => 1,
399 label => 'Last login',
403 label => 'Last logout',
408 label => 'Communigate aliases',
410 disable_inventory => 1,
415 label => 'Communigate account type',
417 select_list => [qw( MultiMailbox TextMailbox MailDirMailbox AGrade BGrade CGrade )],
418 disable_inventory => 1,
421 'cgp_accessmodes' => {
422 label => 'Communigate enabled services',
423 type => 'communigate_pro-accessmodes',
424 disable_inventory => 1,
427 'cgp_rulesallowed' => {
428 label => 'Allowed mail rules',
430 select_list => [ '', 'No', 'Filter Only', 'All But Exec', 'Any' ],
431 disable_inventory => 1,
434 'cgp_rpopallowed' => { label => 'RPOP modifications',
437 'cgp_mailtoall' => { label => 'Accepts mail to "all"',
440 'cgp_addmailtrailer' => { label => 'Add trailer to sent mail',
443 #XXX archive messages, mailing lists
446 'cgp_deletemode' => {
447 label => 'Communigate message delete method',
449 select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
450 disable_inventory => 1,
453 'cgp_emptytrash' => {
454 label => 'Communigate on logout remove trash',
456 select_list => __PACKAGE__->cgp_emptytrash_values,
457 disable_inventory => 1,
461 label => 'Communigate language',
463 select_list => [ '', qw( English Arabic Chinese Dutch French German Hebrew Italian Japanese Portuguese Russian Slovak Spanish Thai ) ],
464 disable_inventory => 1,
468 label => 'Communigate time zone',
470 select_list => __PACKAGE__->cgp_timezone_values,
471 disable_inventory => 1,
475 label => 'Communigate layout',
477 select_list => [ '', '***', 'GoldFleece', 'Skin2' ],
478 disable_inventory => 1,
481 'cgp_prontoskinname' => {
482 label => 'Communigate Pronto style',
484 select_list => [ '', 'Pronto', 'Pronto-darkflame', 'Pronto-steel', 'Pronto-twilight', ],
485 disable_inventory => 1,
488 'cgp_sendmdnmode' => {
489 label => 'Communigate send read receipts',
491 select_list => [ '', 'Never', 'Manually', 'Automatically' ],
492 disable_inventory => 1,
497 #XXX vacation message, redirect all mail, mail rules
504 sub table { 'svc_acct'; }
506 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
510 #false laziness with edit/svc_acct.cgi
512 my( $self, $groups ) = @_;
513 if ( ref($groups) eq 'ARRAY' ) {
515 } elsif ( length($groups) ) {
516 [ split(/\s*,\s*/, $groups) ];
525 shift->_lastlog('in', @_);
529 shift->_lastlog('out', @_);
533 my( $self, $op, $time ) = @_;
535 if ( defined($time) ) {
536 warn "$me last_log$op called on svcnum ". $self->svcnum.
537 ' ('. $self->email. "): $time\n"
542 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
546 my $sth = $dbh->prepare( $sql )
547 or die "Error preparing $sql: ". $dbh->errstr;
548 my $rv = $sth->execute($time, $self->svcnum);
549 die "Error executing $sql: ". $sth->errstr
551 die "Can't update last_log$op for svcnum". $self->svcnum
554 $self->{'Hash'}->{"last_log$op"} = $time;
556 $self->getfield("last_log$op");
560 =item search_sql STRING
562 Class method which returns an SQL fragment to search for the given string.
567 my( $class, $string ) = @_;
568 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
569 my( $username, $domain ) = ( $1, $2 );
570 my $q_username = dbh->quote($username);
571 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
573 "svc_acct.username = $q_username AND ( ".
574 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
579 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
581 $class->search_sql_field('slipip', $string ).
583 $class->search_sql_field('username', $string ).
586 $class->search_sql_field('username', $string);
590 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
592 Returns the "username@domain" string for this account.
594 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
604 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
606 Returns a longer string label for this acccount ("Real Name <username@domain>"
607 if available, or "username@domain").
609 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
616 my $label = $self->label(@_);
617 my $finger = $self->finger;
618 return $label unless $finger =~ /\S/;
619 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
620 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
624 =item insert [ , OPTION => VALUE ... ]
626 Adds this account to the database. If there is an error, returns the error,
627 otherwise returns false.
629 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
630 defined. An FS::cust_svc record will be created and inserted.
632 The additional field I<usergroup> can optionally be defined; if so it should
633 contain an arrayref of group names. See L<FS::radius_usergroup>.
635 The additional field I<child_objects> can optionally be defined; if so it
636 should contain an arrayref of FS::tablename objects. They will have their
637 svcnum fields set and will be inserted after this record, but before any
638 exports are run. Each element of the array can also optionally be a
639 two-element array reference containing the child object and the name of an
640 alternate field to be filled in with the newly-inserted svcnum, for example
641 C<[ $svc_forward, 'srcsvc' ]>
643 Currently available options are: I<depend_jobnum>
645 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
646 jobnums), all provisioning jobs will have a dependancy on the supplied
647 jobnum(s) (they will not run until the specific job(s) complete(s)).
649 (TODOC: L<FS::queue> and L<freeside-queued>)
651 (TODOC: new exports!)
660 warn "[$me] insert called on $self: ". Dumper($self).
661 "\nwith options: ". Dumper(%options);
664 local $SIG{HUP} = 'IGNORE';
665 local $SIG{INT} = 'IGNORE';
666 local $SIG{QUIT} = 'IGNORE';
667 local $SIG{TERM} = 'IGNORE';
668 local $SIG{TSTP} = 'IGNORE';
669 local $SIG{PIPE} = 'IGNORE';
671 my $oldAutoCommit = $FS::UID::AutoCommit;
672 local $FS::UID::AutoCommit = 0;
676 my $error = $self->SUPER::insert(
677 'jobnums' => \@jobnums,
678 'child_objects' => $self->child_objects,
682 $dbh->rollback if $oldAutoCommit;
686 if ( $self->usergroup ) {
687 foreach my $groupname ( @{$self->usergroup} ) {
688 my $radius_usergroup = new FS::radius_usergroup ( {
689 svcnum => $self->svcnum,
690 groupname => $groupname,
692 my $error = $radius_usergroup->insert;
694 $dbh->rollback if $oldAutoCommit;
700 unless ( $skip_fuzzyfiles ) {
701 $error = $self->queue_fuzzyfiles_update;
703 $dbh->rollback if $oldAutoCommit;
704 return "updating fuzzy search cache: $error";
708 my $cust_pkg = $self->cust_svc->cust_pkg;
711 my $cust_main = $cust_pkg->cust_main;
712 my $agentnum = $cust_main->agentnum;
714 if ( $conf->exists('emailinvoiceautoalways')
715 || $conf->exists('emailinvoiceauto')
716 && ! $cust_main->invoicing_list_emailonly
718 my @invoicing_list = $cust_main->invoicing_list;
719 push @invoicing_list, $self->email;
720 $cust_main->invoicing_list(\@invoicing_list);
724 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
725 = ('','','','','','');
727 if ( $conf->exists('welcome_email', $agentnum) ) {
728 $welcome_template = new Text::Template (
730 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
731 ) or warn "can't create welcome email template: $Text::Template::ERROR";
732 $welcome_from = $conf->config('welcome_email-from', $agentnum);
733 # || 'your-isp-is-dum'
734 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
736 $welcome_subject_template = new Text::Template (
738 SOURCE => $welcome_subject,
739 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
740 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
743 if ( $welcome_template && $cust_pkg ) {
744 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
748 'custnum' => $self->custnum,
749 'username' => $self->username,
750 'password' => $self->_password,
751 'first' => $cust_main->first,
752 'last' => $cust_main->getfield('last'),
753 'pkg' => $cust_pkg->part_pkg->pkg,
755 my $wqueue = new FS::queue {
756 'svcnum' => $self->svcnum,
757 'job' => 'FS::svc_acct::send_email'
759 my $error = $wqueue->insert(
761 'from' => $welcome_from,
762 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
763 'mimetype' => $welcome_mimetype,
764 'body' => $welcome_template->fill_in( HASH => \%hash, ),
767 $dbh->rollback if $oldAutoCommit;
768 return "error queuing welcome email: $error";
771 if ( $options{'depend_jobnum'} ) {
772 warn "$me depend_jobnum found; adding to welcome email dependancies"
774 if ( ref($options{'depend_jobnum'}) ) {
775 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
776 "to welcome email dependancies"
778 push @jobnums, @{ $options{'depend_jobnum'} };
780 warn "$me adding job $options{'depend_jobnum'} ".
781 "to welcome email dependancies"
783 push @jobnums, $options{'depend_jobnum'};
787 foreach my $jobnum ( @jobnums ) {
788 my $error = $wqueue->depend_insert($jobnum);
790 $dbh->rollback if $oldAutoCommit;
791 return "error queuing welcome email job dependancy: $error";
801 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
805 # set usage fields and thresholds if unset but set in a package def
806 # AND the package already has a last bill date (otherwise they get double added)
807 sub preinsert_hook_first {
810 return '' unless $self->pkgnum;
812 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
813 return '' unless $cust_pkg && $cust_pkg->last_bill;
815 my $part_pkg = $cust_pkg->part_pkg;
816 return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
818 my %values = $part_pkg->usage_valuehash;
819 my $multiplier = $conf->exists('svc_acct-usage_threshold')
820 ? 1 - $conf->config('svc_acct-usage_threshold')/100
821 : 0.20; #doesn't matter
823 foreach ( keys %values ) {
824 next if $self->getfield($_);
825 $self->setfield( $_, $values{$_} );
826 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
827 if $conf->exists('svc_acct-usage_threshold');
835 Deletes this account from the database. If there is an error, returns the
836 error, otherwise returns false.
838 The corresponding FS::cust_svc record will be deleted as well.
840 (TODOC: new exports!)
847 return "can't delete system account" if $self->_check_system;
849 return "Can't delete an account which is a (svc_forward) source!"
850 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
852 return "Can't delete an account which is a (svc_forward) destination!"
853 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
855 return "Can't delete an account with (svc_www) web service!"
856 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
858 # what about records in session ? (they should refer to history table)
860 local $SIG{HUP} = 'IGNORE';
861 local $SIG{INT} = 'IGNORE';
862 local $SIG{QUIT} = 'IGNORE';
863 local $SIG{TERM} = 'IGNORE';
864 local $SIG{TSTP} = 'IGNORE';
865 local $SIG{PIPE} = 'IGNORE';
867 my $oldAutoCommit = $FS::UID::AutoCommit;
868 local $FS::UID::AutoCommit = 0;
871 foreach my $cust_main_invoice (
872 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
874 unless ( defined($cust_main_invoice) ) {
875 warn "WARNING: something's wrong with qsearch";
878 my %hash = $cust_main_invoice->hash;
879 $hash{'dest'} = $self->email;
880 my $new = new FS::cust_main_invoice \%hash;
881 my $error = $new->replace($cust_main_invoice);
883 $dbh->rollback if $oldAutoCommit;
888 foreach my $svc_domain (
889 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
891 my %hash = new FS::svc_domain->hash;
892 $hash{'catchall'} = '';
893 my $new = new FS::svc_domain \%hash;
894 my $error = $new->replace($svc_domain);
896 $dbh->rollback if $oldAutoCommit;
901 my $error = $self->SUPER::delete;
903 $dbh->rollback if $oldAutoCommit;
907 foreach my $radius_usergroup (
908 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
910 my $error = $radius_usergroup->delete;
912 $dbh->rollback if $oldAutoCommit;
917 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
921 =item replace OLD_RECORD
923 Replaces OLD_RECORD with this one in the database. If there is an error,
924 returns the error, otherwise returns false.
926 The additional field I<usergroup> can optionally be defined; if so it should
927 contain an arrayref of group names. See L<FS::radius_usergroup>.
935 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
939 warn "$me replacing $old with $new\n" if $DEBUG;
943 return "can't modify system account" if $old->_check_system;
946 #no warnings 'numeric'; #alas, a 5.006-ism
949 foreach my $xid (qw( uid gid )) {
951 return "Can't change $xid!"
952 if ! $conf->exists("svc_acct-edit_$xid")
953 && $old->$xid() != $new->$xid()
954 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
959 #change homdir when we change username
960 $new->setfield('dir', '') if $old->username ne $new->username;
962 local $SIG{HUP} = 'IGNORE';
963 local $SIG{INT} = 'IGNORE';
964 local $SIG{QUIT} = 'IGNORE';
965 local $SIG{TERM} = 'IGNORE';
966 local $SIG{TSTP} = 'IGNORE';
967 local $SIG{PIPE} = 'IGNORE';
969 my $oldAutoCommit = $FS::UID::AutoCommit;
970 local $FS::UID::AutoCommit = 0;
973 # redundant, but so $new->usergroup gets set
974 $error = $new->check;
975 return $error if $error;
977 $old->usergroup( [ $old->radius_groups ] );
979 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
980 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
982 if ( $new->usergroup ) {
983 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
984 my @newgroups = @{$new->usergroup};
985 foreach my $oldgroup ( @{$old->usergroup} ) {
986 if ( grep { $oldgroup eq $_ } @newgroups ) {
987 @newgroups = grep { $oldgroup ne $_ } @newgroups;
990 my $radius_usergroup = qsearchs('radius_usergroup', {
991 svcnum => $old->svcnum,
992 groupname => $oldgroup,
994 my $error = $radius_usergroup->delete;
996 $dbh->rollback if $oldAutoCommit;
997 return "error deleting radius_usergroup $oldgroup: $error";
1001 foreach my $newgroup ( @newgroups ) {
1002 my $radius_usergroup = new FS::radius_usergroup ( {
1003 svcnum => $new->svcnum,
1004 groupname => $newgroup,
1006 my $error = $radius_usergroup->insert;
1008 $dbh->rollback if $oldAutoCommit;
1009 return "error adding radius_usergroup $newgroup: $error";
1015 $error = $new->SUPER::replace($old, @_);
1017 $dbh->rollback if $oldAutoCommit;
1018 return $error if $error;
1021 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
1022 $error = $new->queue_fuzzyfiles_update;
1024 $dbh->rollback if $oldAutoCommit;
1025 return "updating fuzzy search cache: $error";
1029 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1033 =item queue_fuzzyfiles_update
1035 Used by insert & replace to update the fuzzy search cache
1039 sub queue_fuzzyfiles_update {
1042 local $SIG{HUP} = 'IGNORE';
1043 local $SIG{INT} = 'IGNORE';
1044 local $SIG{QUIT} = 'IGNORE';
1045 local $SIG{TERM} = 'IGNORE';
1046 local $SIG{TSTP} = 'IGNORE';
1047 local $SIG{PIPE} = 'IGNORE';
1049 my $oldAutoCommit = $FS::UID::AutoCommit;
1050 local $FS::UID::AutoCommit = 0;
1053 my $queue = new FS::queue {
1054 'svcnum' => $self->svcnum,
1055 'job' => 'FS::svc_acct::append_fuzzyfiles'
1057 my $error = $queue->insert($self->username);
1059 $dbh->rollback if $oldAutoCommit;
1060 return "queueing job (transaction rolled back): $error";
1063 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1071 Suspends this account by calling export-specific suspend hooks. If there is
1072 an error, returns the error, otherwise returns false.
1074 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1080 return "can't suspend system account" if $self->_check_system;
1081 $self->SUPER::suspend(@_);
1086 Unsuspends this account by by calling export-specific suspend hooks. If there
1087 is an error, returns the error, otherwise returns false.
1089 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1095 my %hash = $self->hash;
1096 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
1097 $hash{_password} = $1;
1098 my $new = new FS::svc_acct ( \%hash );
1099 my $error = $new->replace($self);
1100 return $error if $error;
1103 $self->SUPER::unsuspend(@_);
1108 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1110 If the B<auto_unset_catchall> configuration option is set, this method will
1111 automatically remove any references to the canceled service in the catchall
1112 field of svc_domain. This allows packages that contain both a svc_domain and
1113 its catchall svc_acct to be canceled in one step.
1118 # Only one thing to do at this level
1120 foreach my $svc_domain (
1121 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
1122 if($conf->exists('auto_unset_catchall')) {
1123 my %hash = $svc_domain->hash;
1124 $hash{catchall} = '';
1125 my $new = new FS::svc_domain ( \%hash );
1126 my $error = $new->replace($svc_domain);
1127 return $error if $error;
1129 return "cannot unprovision svc_acct #".$self->svcnum.
1130 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
1134 $self->SUPER::cancel(@_);
1140 Checks all fields to make sure this is a valid service. If there is an error,
1141 returns the error, otherwise returns false. Called by the insert and replace
1144 Sets any fixed values; see L<FS::part_svc>.
1151 my($recref) = $self->hashref;
1153 my $x = $self->setfixed( $self->_fieldhandlers );
1154 return $x unless ref($x);
1157 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1159 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1162 my $error = $self->ut_numbern('svcnum')
1163 #|| $self->ut_number('domsvc')
1164 || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
1165 || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
1166 || $self->ut_textn('sec_phrase')
1167 || $self->ut_snumbern('seconds')
1168 || $self->ut_snumbern('upbytes')
1169 || $self->ut_snumbern('downbytes')
1170 || $self->ut_snumbern('totalbytes')
1171 || $self->ut_snumbern('seconds_threshold')
1172 || $self->ut_snumbern('upbytes_threshold')
1173 || $self->ut_snumbern('downbytes_threshold')
1174 || $self->ut_snumbern('totalbytes_threshold')
1175 || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
1176 || $self->ut_enum('password_selfchange', [ '', 'Y' ])
1177 || $self->ut_enum('password_recover', [ '', 'Y' ])
1178 || $self->ut_textn('cgp_accessmodes')
1179 || $self->ut_alphan('cgp_type')
1180 || $self->ut_textn('cgp_aliases' ) #well
1182 || $self->ut_alphasn('cgp_rulesallowed')
1183 || $self->ut_enum('cgp_rpopallowed', [ '', 'Y' ])
1184 || $self->ut_enum('cgp_mailtoall', [ '', 'Y' ])
1185 || $self->ut_enum('cgp_addmailtrailer', [ '', 'Y' ])
1187 || $self->ut_alphasn('cgp_deletemode')
1188 || $self->ut_enum('cgp_emptytrash', $self->cgp_emptytrash_values)
1189 || $self->ut_alphan('cgp_language')
1190 || $self->ut_textn('cgp_timezone')
1191 || $self->ut_textn('cgp_skinname')
1192 || $self->ut_textn('cgp_prontoskinname')
1193 || $self->ut_alphan('cgp_sendmdnmode')
1194 #XXX vacation message, redirect all mail, mail rules
1197 return $error if $error;
1200 local $username_letter = $username_letter;
1201 if ($self->svcnum) {
1202 my $cust_svc = $self->cust_svc
1203 or return "no cust_svc record found for svcnum ". $self->svcnum;
1204 my $cust_pkg = $cust_svc->cust_pkg;
1206 if ($self->pkgnum) {
1207 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );#complain?
1211 $conf->exists('username-letter', $cust_pkg->cust_main->agentnum);
1214 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1215 if ( $username_uppercase ) {
1216 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1217 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1218 $recref->{username} = $1;
1220 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1221 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1222 $recref->{username} = $1;
1225 if ( $username_letterfirst ) {
1226 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1227 } elsif ( $username_letter ) {
1228 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1230 if ( $username_noperiod ) {
1231 $recref->{username} =~ /\./ and return gettext('illegal_username');
1233 if ( $username_nounderscore ) {
1234 $recref->{username} =~ /_/ and return gettext('illegal_username');
1236 if ( $username_nodash ) {
1237 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1239 unless ( $username_ampersand ) {
1240 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1242 unless ( $username_percent ) {
1243 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1245 unless ( $username_colon ) {
1246 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1249 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1250 $recref->{popnum} = $1;
1251 return "Unknown popnum" unless
1252 ! $recref->{popnum} ||
1253 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1255 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1257 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1258 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1260 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1261 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1262 #not all systems use gid=uid
1263 #you can set a fixed gid in part_svc
1265 return "Only root can have uid 0"
1266 if $recref->{uid} == 0
1267 && $recref->{username} !~ /^(root|toor|smtp)$/;
1269 unless ( $recref->{username} eq 'sync' ) {
1270 if ( grep $_ eq $recref->{shell}, @shells ) {
1271 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1273 return "Illegal shell \`". $self->shell. "\'; ".
1274 "shells configuration value contains: @shells";
1277 $recref->{shell} = '/bin/sync';
1281 $recref->{gid} ne '' ?
1282 return "Can't have gid without uid" : ( $recref->{gid}='' );
1283 #$recref->{dir} ne '' ?
1284 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1285 $recref->{shell} ne '' ?
1286 return "Can't have shell without uid" : ( $recref->{shell}='' );
1289 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1291 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1292 or return "Illegal directory: ". $recref->{dir};
1293 $recref->{dir} = $1;
1294 return "Illegal directory"
1295 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1296 return "Illegal directory"
1297 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1298 unless ( $recref->{dir} ) {
1299 $recref->{dir} = $dir_prefix . '/';
1300 if ( $dirhash > 0 ) {
1301 for my $h ( 1 .. $dirhash ) {
1302 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1304 } elsif ( $dirhash < 0 ) {
1305 for my $h ( reverse $dirhash .. -1 ) {
1306 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1309 $recref->{dir} .= $recref->{username};
1315 # $error = $self->ut_textn('finger');
1316 # return $error if $error;
1317 if ( $self->getfield('finger') eq '' ) {
1318 my $cust_pkg = $self->svcnum
1319 ? $self->cust_svc->cust_pkg
1320 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1322 my $cust_main = $cust_pkg->cust_main;
1323 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1326 $self->getfield('finger') =~
1327 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1328 or return "Illegal finger: ". $self->getfield('finger');
1329 $self->setfield('finger', $1);
1331 for (qw( quota file_quota file_maxsize )) {
1332 $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
1335 $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
1336 $recref->{file_maxnum} = $1;
1338 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1339 if ( $recref->{slipip} eq '' ) {
1340 $recref->{slipip} = '';
1341 } elsif ( $recref->{slipip} eq '0e0' ) {
1342 $recref->{slipip} = '0e0';
1344 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1345 or return "Illegal slipip: ". $self->slipip;
1346 $recref->{slipip} = $1;
1351 #arbitrary RADIUS stuff; allow ut_textn for now
1352 foreach ( grep /^radius_/, fields('svc_acct') ) {
1353 $self->ut_textn($_);
1356 # First, if _password is blank, generate one and set default encoding.
1357 if ( ! $recref->{_password} ) {
1358 $error = $self->set_password('');
1360 # But if there's a _password but no encoding, assume it's plaintext and
1361 # set it to default encoding.
1362 elsif ( ! $recref->{_password_encoding} ) {
1363 $error = $self->set_password($recref->{_password});
1365 return $error if $error;
1367 # Next, check _password to ensure compliance with the encoding.
1368 if ( $recref->{_password_encoding} eq 'ldap' ) {
1370 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1371 $recref->{_password} = uc($1).$2;
1373 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1376 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1378 if ( $recref->{_password} =~
1379 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1380 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1383 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1386 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1389 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1390 # Password randomization is now in set_password.
1391 # Strip whitespace characters, check length requirements, etc.
1392 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1393 $recref->{_password} = $1;
1395 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1396 FS::Msgcat::_gettext('illegal_password_characters').
1397 ": ". $recref->{_password};
1400 if ( $password_noampersand ) {
1401 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1403 if ( $password_noexclamation ) {
1404 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1408 return "invalid password encoding ('".$recref->{_password_encoding}."'";
1410 $self->SUPER::check;
1415 sub _password_encryption {
1417 my $encoding = lc($self->_password_encoding);
1418 return if !$encoding;
1419 return 'plain' if $encoding eq 'plain';
1420 if($encoding eq 'crypt') {
1421 my $pass = $self->_password;
1422 $pass =~ s/^\*SUSPENDED\* //;
1424 return 'md5' if $pass =~ /^\$1\$/;
1425 #return 'blowfish' if $self->_password =~ /^\$2\$/;
1426 return 'des' if length($pass) == 13;
1429 if($encoding eq 'ldap') {
1430 uc($self->_password) =~ /^\{([\w-]+)\}/;
1431 return 'crypt' if $1 eq 'CRYPT' or $1 eq 'DES';
1432 return 'plain' if $1 eq 'PLAIN' or $1 eq 'CLEARTEXT';
1433 return 'md5' if $1 eq 'MD5';
1434 return 'sha1' if $1 eq 'SHA' or $1 eq 'SHA-1';
1441 sub get_cleartext_password {
1443 if($self->_password_encryption eq 'plain') {
1444 if($self->_password_encoding eq 'ldap') {
1445 $self->_password =~ /\{\w+\}(.*)$/;
1449 return $self->_password;
1458 Set the cleartext password for the account. If _password_encoding is set, the
1459 new password will be encoded according to the existing method (including
1460 encryption mode, if it can be determined). Otherwise,
1461 config('default-password-encoding') is used.
1463 If no password is supplied (or a zero-length password when minimum password length
1464 is >0), one will be generated randomly.
1469 my( $self, $pass ) = ( shift, shift );
1471 warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
1474 my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
1475 FS::Msgcat::_gettext('illegal_password_characters').
1478 my( $encoding, $encryption ) = ('', '');
1480 if ( $self->_password_encoding ) {
1481 $encoding = $self->_password_encoding;
1482 # identify existing encryption method, try to use it.
1483 $encryption = $self->_password_encryption;
1485 # use the system default
1491 # set encoding to system default
1492 ($encoding, $encryption) =
1493 split(/-/, lc($conf->config('default-password-encoding')));
1494 $encoding ||= 'legacy';
1495 $self->_password_encoding($encoding);
1498 if ( $encoding eq 'legacy' ) {
1500 # The legacy behavior from check():
1501 # If the password is blank, randomize it and set encoding to 'plain'.
1502 if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
1503 $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1504 $self->_password_encoding('plain');
1506 # Prefix + valid-length password
1507 if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1509 $self->_password_encoding('plain');
1510 # Prefix + crypt string
1511 } elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1513 $self->_password_encoding('crypt');
1514 # Various disabled crypt passwords
1515 } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
1516 $self->_password_encoding('crypt');
1522 $self->_password($pass);
1528 if $passwordmin && length($pass) < $passwordmin
1529 or $passwordmax && length($pass) > $passwordmax;
1531 if ( $encoding eq 'crypt' ) {
1532 if ($encryption eq 'md5') {
1533 $pass = unix_md5_crypt($pass);
1534 } elsif ($encryption eq 'des') {
1535 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1538 } elsif ( $encoding eq 'ldap' ) {
1539 if ($encryption eq 'md5') {
1540 $pass = md5_base64($pass);
1541 } elsif ($encryption eq 'sha1') {
1542 $pass = sha1_base64($pass);
1543 } elsif ($encryption eq 'crypt') {
1544 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1546 # else $encryption eq 'plain', do nothing
1547 $pass = '{'.uc($encryption).'}'.$pass;
1549 # else encoding eq 'plain'
1551 $self->_password($pass);
1557 Internal function to check the username against the list of system usernames
1558 from the I<system_usernames> configuration value. Returns true if the username
1559 is listed on the system username list.
1565 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1566 $conf->config('system_usernames')
1570 =item _check_duplicate
1572 Internal method to check for duplicates usernames, username@domain pairs and
1575 If the I<global_unique-username> configuration value is set to B<username> or
1576 B<username@domain>, enforces global username or username@domain uniqueness.
1578 In all cases, check for duplicate uids and usernames or username@domain pairs
1579 per export and with identical I<svcpart> values.
1583 sub _check_duplicate {
1586 my $global_unique = $conf->config('global_unique-username') || 'none';
1587 return '' if $global_unique eq 'disabled';
1591 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1592 unless ( $part_svc ) {
1593 return 'unknown svcpart '. $self->svcpart;
1596 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1597 qsearch( 'svc_acct', { 'username' => $self->username } );
1598 return gettext('username_in_use')
1599 if $global_unique eq 'username' && @dup_user;
1601 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1602 qsearch( 'svc_acct', { 'username' => $self->username,
1603 'domsvc' => $self->domsvc } );
1604 return gettext('username_in_use')
1605 if $global_unique eq 'username@domain' && @dup_userdomain;
1608 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1609 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1610 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1611 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1616 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1617 my $exports = FS::part_export::export_info('svc_acct');
1618 my %conflict_user_svcpart;
1619 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1621 foreach my $part_export ( $part_svc->part_export ) {
1623 #this will catch to the same exact export
1624 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1626 #this will catch to exports w/same exporthost+type ???
1627 #my @other_part_export = qsearch('part_export', {
1628 # 'machine' => $part_export->machine,
1629 # 'exporttype' => $part_export->exporttype,
1631 #foreach my $other_part_export ( @other_part_export ) {
1632 # push @svcparts, map { $_->svcpart }
1633 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1636 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1637 #silly kludge to avoid uninitialized value errors
1638 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1639 ? $exports->{$part_export->exporttype}{'nodomain'}
1641 if ( $nodomain =~ /^Y/i ) {
1642 $conflict_user_svcpart{$_} = $part_export->exportnum
1645 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1650 foreach my $dup_user ( @dup_user ) {
1651 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1652 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1653 return "duplicate username ". $self->username.
1654 ": conflicts with svcnum ". $dup_user->svcnum.
1655 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1659 foreach my $dup_userdomain ( @dup_userdomain ) {
1660 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1661 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1662 return "duplicate username\@domain ". $self->email.
1663 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1664 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1668 foreach my $dup_uid ( @dup_uid ) {
1669 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1670 if ( exists($conflict_user_svcpart{$dup_svcpart})
1671 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1672 return "duplicate uid ". $self->uid.
1673 ": conflicts with svcnum ". $dup_uid->svcnum.
1675 ( $conflict_user_svcpart{$dup_svcpart}
1676 || $conflict_userdomain_svcpart{$dup_svcpart} );
1688 Depriciated, use radius_reply instead.
1693 carp "FS::svc_acct::radius depriciated, use radius_reply";
1694 $_[0]->radius_reply;
1699 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1700 reply attributes of this record.
1702 Note that this is now the preferred method for reading RADIUS attributes -
1703 accessing the columns directly is discouraged, as the column names are
1704 expected to change in the future.
1711 return %{ $self->{'radius_reply'} }
1712 if exists $self->{'radius_reply'};
1717 my($column, $attrib) = ($1, $2);
1718 #$attrib =~ s/_/\-/g;
1719 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1720 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1722 if ( $self->slipip && $self->slipip ne '0e0' ) {
1723 $reply{$radius_ip} = $self->slipip;
1726 if ( $self->seconds !~ /^$/ ) {
1727 $reply{'Session-Timeout'} = $self->seconds;
1730 if ( $conf->exists('radius-chillispot-max') ) {
1731 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1733 #hmm. just because sqlradius.pm says so?
1740 foreach my $what (qw( input output total )) {
1741 my $is = $whatis{$what}.'bytes';
1742 if ( $self->$is() =~ /\d/ ) {
1743 my $big = new Math::BigInt $self->$is();
1744 $big = new Math::BigInt '0' if $big->is_neg();
1745 my $att = "Chillispot-Max-\u$what";
1746 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1747 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1758 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1759 check attributes of this record.
1761 Note that this is now the preferred method for reading RADIUS attributes -
1762 accessing the columns directly is discouraged, as the column names are
1763 expected to change in the future.
1770 return %{ $self->{'radius_check'} }
1771 if exists $self->{'radius_check'};
1776 my($column, $attrib) = ($1, $2);
1777 #$attrib =~ s/_/\-/g;
1778 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1779 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1782 my($pw_attrib, $password) = $self->radius_password;
1783 $check{$pw_attrib} = $password;
1785 my $cust_svc = $self->cust_svc;
1787 my $cust_pkg = $cust_svc->cust_pkg;
1788 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1789 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1792 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1793 "; can't set Expiration\n"
1801 =item radius_password
1803 Returns a key/value pair containing the RADIUS attribute name and value
1808 sub radius_password {
1812 if ( $self->_password_encoding eq 'ldap' ) {
1813 $pw_attrib = 'Password-With-Header';
1814 } elsif ( $self->_password_encoding eq 'crypt' ) {
1815 $pw_attrib = 'Crypt-Password';
1816 } elsif ( $self->_password_encoding eq 'plain' ) {
1817 $pw_attrib = $radius_password;
1819 $pw_attrib = length($self->_password) <= 12
1824 ($pw_attrib, $self->_password);
1830 This method instructs the object to "snapshot" or freeze RADIUS check and
1831 reply attributes to the current values.
1835 #bah, my english is too broken this morning
1836 #Of note is the "Expiration" attribute, which, for accounts in prepaid packages, is typically defined on-the-fly as the associated packages cust_pkg.bill. (This is used by
1837 #the FS::cust_pkg's replace method to trigger the correct export updates when
1838 #package dates change)
1843 $self->{$_} = { $self->$_() }
1844 foreach qw( radius_reply radius_check );
1848 =item forget_snapshot
1850 This methos instructs the object to forget any previously snapshotted
1851 RADIUS check and reply attributes.
1855 sub forget_snapshot {
1859 foreach qw( radius_reply radius_check );
1863 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1865 Returns the domain associated with this account.
1867 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1874 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1875 my $svc_domain = $self->svc_domain(@_)
1876 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1877 $svc_domain->domain;
1882 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1886 #inherited from svc_Common
1888 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1890 Returns an email address associated with the account.
1892 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1899 $self->username. '@'. $self->domain(@_);
1904 Returns an array of FS::acct_snarf records associated with the account.
1905 If the acct_snarf table does not exist or there are no associated records,
1906 an empty list is returned
1912 return () unless dbdef->table('acct_snarf');
1913 eval "use FS::acct_snarf;";
1915 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1918 =item decrement_upbytes OCTETS
1920 Decrements the I<upbytes> field of this record by the given amount. If there
1921 is an error, returns the error, otherwise returns false.
1925 sub decrement_upbytes {
1926 shift->_op_usage('-', 'upbytes', @_);
1929 =item increment_upbytes OCTETS
1931 Increments the I<upbytes> field of this record by the given amount. If there
1932 is an error, returns the error, otherwise returns false.
1936 sub increment_upbytes {
1937 shift->_op_usage('+', 'upbytes', @_);
1940 =item decrement_downbytes OCTETS
1942 Decrements the I<downbytes> field of this record by the given amount. If there
1943 is an error, returns the error, otherwise returns false.
1947 sub decrement_downbytes {
1948 shift->_op_usage('-', 'downbytes', @_);
1951 =item increment_downbytes OCTETS
1953 Increments the I<downbytes> field of this record by the given amount. If there
1954 is an error, returns the error, otherwise returns false.
1958 sub increment_downbytes {
1959 shift->_op_usage('+', 'downbytes', @_);
1962 =item decrement_totalbytes OCTETS
1964 Decrements the I<totalbytes> field of this record by the given amount. If there
1965 is an error, returns the error, otherwise returns false.
1969 sub decrement_totalbytes {
1970 shift->_op_usage('-', 'totalbytes', @_);
1973 =item increment_totalbytes OCTETS
1975 Increments the I<totalbytes> field of this record by the given amount. If there
1976 is an error, returns the error, otherwise returns false.
1980 sub increment_totalbytes {
1981 shift->_op_usage('+', 'totalbytes', @_);
1984 =item decrement_seconds SECONDS
1986 Decrements the I<seconds> field of this record by the given amount. If there
1987 is an error, returns the error, otherwise returns false.
1991 sub decrement_seconds {
1992 shift->_op_usage('-', 'seconds', @_);
1995 =item increment_seconds SECONDS
1997 Increments the I<seconds> field of this record by the given amount. If there
1998 is an error, returns the error, otherwise returns false.
2002 sub increment_seconds {
2003 shift->_op_usage('+', 'seconds', @_);
2011 my %op2condition = (
2012 '-' => sub { my($self, $column, $amount) = @_;
2013 $self->$column - $amount <= 0;
2015 '+' => sub { my($self, $column, $amount) = @_;
2016 ($self->$column || 0) + $amount > 0;
2019 my %op2warncondition = (
2020 '-' => sub { my($self, $column, $amount) = @_;
2021 my $threshold = $column . '_threshold';
2022 $self->$column - $amount <= $self->$threshold + 0;
2024 '+' => sub { my($self, $column, $amount) = @_;
2025 ($self->$column || 0) + $amount > 0;
2030 my( $self, $op, $column, $amount ) = @_;
2032 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
2033 ' ('. $self->email. "): $op $amount\n"
2036 return '' unless $amount;
2038 local $SIG{HUP} = 'IGNORE';
2039 local $SIG{INT} = 'IGNORE';
2040 local $SIG{QUIT} = 'IGNORE';
2041 local $SIG{TERM} = 'IGNORE';
2042 local $SIG{TSTP} = 'IGNORE';
2043 local $SIG{PIPE} = 'IGNORE';
2045 my $oldAutoCommit = $FS::UID::AutoCommit;
2046 local $FS::UID::AutoCommit = 0;
2049 my $sql = "UPDATE svc_acct SET $column = ".
2050 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
2051 " $op ? WHERE svcnum = ?";
2055 my $sth = $dbh->prepare( $sql )
2056 or die "Error preparing $sql: ". $dbh->errstr;
2057 my $rv = $sth->execute($amount, $self->svcnum);
2058 die "Error executing $sql: ". $sth->errstr
2059 unless defined($rv);
2060 die "Can't update $column for svcnum". $self->svcnum
2063 #$self->snapshot; #not necessary, we retain the old values
2064 #create an object with the updated usage values
2065 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2067 my $error = $new->replace($self);
2069 $dbh->rollback if $oldAutoCommit;
2070 return "Error replacing: $error";
2073 #overlimit_action eq 'cancel' handling
2074 my $cust_pkg = $self->cust_svc->cust_pkg;
2076 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
2077 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
2081 my $error = $cust_pkg->cancel; #XXX should have a reason
2083 $dbh->rollback if $oldAutoCommit;
2084 return "Error cancelling: $error";
2087 #nothing else is relevant if we're cancelling, so commit & return success
2088 warn "$me update successful; committing\n"
2090 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2095 my $action = $op2action{$op};
2097 if ( &{$op2condition{$op}}($self, $column, $amount) &&
2098 ( $action eq 'suspend' && !$self->overlimit
2099 || $action eq 'unsuspend' && $self->overlimit )
2102 my $error = $self->_op_overlimit($action);
2104 $dbh->rollback if $oldAutoCommit;
2110 if ( $conf->exists("svc_acct-usage_$action")
2111 && &{$op2condition{$op}}($self, $column, $amount) ) {
2112 #my $error = $self->$action();
2113 my $error = $self->cust_svc->cust_pkg->$action();
2114 # $error ||= $self->overlimit($action);
2116 $dbh->rollback if $oldAutoCommit;
2117 return "Error ${action}ing: $error";
2121 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
2122 my $wqueue = new FS::queue {
2123 'svcnum' => $self->svcnum,
2124 'job' => 'FS::svc_acct::reached_threshold',
2129 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
2133 my $error = $wqueue->insert(
2134 'svcnum' => $self->svcnum,
2136 'column' => $column,
2140 $dbh->rollback if $oldAutoCommit;
2141 return "Error queuing threshold activity: $error";
2145 warn "$me update successful; committing\n"
2147 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2153 my( $self, $action ) = @_;
2155 local $SIG{HUP} = 'IGNORE';
2156 local $SIG{INT} = 'IGNORE';
2157 local $SIG{QUIT} = 'IGNORE';
2158 local $SIG{TERM} = 'IGNORE';
2159 local $SIG{TSTP} = 'IGNORE';
2160 local $SIG{PIPE} = 'IGNORE';
2162 my $oldAutoCommit = $FS::UID::AutoCommit;
2163 local $FS::UID::AutoCommit = 0;
2166 my $cust_pkg = $self->cust_svc->cust_pkg;
2168 my $conf_overlimit =
2170 ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
2171 : $conf->config('overlimit_groups');
2173 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
2175 my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
2176 next unless $groups;
2178 my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
2180 my $other = new FS::svc_acct $self->hashref;
2181 $other->usergroup( $gref );
2184 if ($action eq 'suspend') {
2187 } else { # $action eq 'unsuspend'
2192 my $error = $part_export->export_replace($new, $old)
2193 || $self->overlimit($action);
2196 $dbh->rollback if $oldAutoCommit;
2197 return "Error replacing radius groups: $error";
2202 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2208 my( $self, $valueref, %options ) = @_;
2210 warn "$me set_usage called for svcnum ". $self->svcnum.
2211 ' ('. $self->email. "): ".
2212 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
2215 local $SIG{HUP} = 'IGNORE';
2216 local $SIG{INT} = 'IGNORE';
2217 local $SIG{QUIT} = 'IGNORE';
2218 local $SIG{TERM} = 'IGNORE';
2219 local $SIG{TSTP} = 'IGNORE';
2220 local $SIG{PIPE} = 'IGNORE';
2222 local $FS::svc_Common::noexport_hack = 1;
2223 my $oldAutoCommit = $FS::UID::AutoCommit;
2224 local $FS::UID::AutoCommit = 0;
2229 if ( $options{null} ) {
2230 %handyhash = ( map { ( $_ => undef, $_."_threshold" => undef ) }
2231 qw( seconds upbytes downbytes totalbytes )
2234 foreach my $field (keys %$valueref){
2235 $reset = 1 if $valueref->{$field};
2236 $self->setfield($field, $valueref->{$field});
2237 $self->setfield( $field.'_threshold',
2238 int($self->getfield($field)
2239 * ( $conf->exists('svc_acct-usage_threshold')
2240 ? 1 - $conf->config('svc_acct-usage_threshold')/100
2245 $handyhash{$field} = $self->getfield($field);
2246 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
2248 #my $error = $self->replace; #NO! we avoid the call to ->check for
2249 #die $error if $error; #services not explicity changed via the UI
2251 my $sql = "UPDATE svc_acct SET " .
2252 join (',', map { "$_ = ?" } (keys %handyhash) ).
2253 " WHERE svcnum = ". $self->svcnum;
2258 if (scalar(keys %handyhash)) {
2259 my $sth = $dbh->prepare( $sql )
2260 or die "Error preparing $sql: ". $dbh->errstr;
2261 my $rv = $sth->execute(values %handyhash);
2262 die "Error executing $sql: ". $sth->errstr
2263 unless defined($rv);
2264 die "Can't update usage for svcnum ". $self->svcnum
2268 #$self->snapshot; #not necessary, we retain the old values
2269 #create an object with the updated usage values
2270 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2271 local($FS::Record::nowarn_identical) = 1;
2272 my $error = $new->replace($self); #call exports
2274 $dbh->rollback if $oldAutoCommit;
2275 return "Error replacing: $error";
2282 $error = $self->_op_overlimit('unsuspend')
2283 if $self->overlimit;;
2285 $error ||= $self->cust_svc->cust_pkg->unsuspend
2286 if $conf->exists("svc_acct-usage_unsuspend");
2289 $dbh->rollback if $oldAutoCommit;
2290 return "Error unsuspending: $error";
2295 warn "$me update successful; committing\n"
2297 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2303 =item recharge HASHREF
2305 Increments usage columns by the amount specified in HASHREF as
2306 column=>amount pairs.
2311 my ($self, $vhash) = @_;
2314 warn "[$me] recharge called on $self: ". Dumper($self).
2315 "\nwith vhash: ". Dumper($vhash);
2318 my $oldAutoCommit = $FS::UID::AutoCommit;
2319 local $FS::UID::AutoCommit = 0;
2323 foreach my $column (keys %$vhash){
2324 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
2328 $dbh->rollback if $oldAutoCommit;
2330 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2335 =item is_rechargeable
2337 Returns true if this svc_account can be "recharged" and false otherwise.
2341 sub is_rechargable {
2343 $self->seconds ne ''
2344 || $self->upbytes ne ''
2345 || $self->downbytes ne ''
2346 || $self->totalbytes ne '';
2349 =item seconds_since TIMESTAMP
2351 Returns the number of seconds this account has been online since TIMESTAMP,
2352 according to the session monitor (see L<FS::Session>).
2354 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2355 L<Time::Local> and L<Date::Parse> for conversion functions.
2359 #note: POD here, implementation in FS::cust_svc
2362 $self->cust_svc->seconds_since(@_);
2365 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
2367 Returns the numbers of seconds this account has been online between
2368 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
2369 external SQL radacct table, specified via sqlradius export. Sessions which
2370 started in the specified range but are still open are counted from session
2371 start to the end of the range (unless they are over 1 day old, in which case
2372 they are presumed missing their stop record and not counted). Also, sessions
2373 which end in the range but started earlier are counted from the start of the
2374 range to session end. Finally, sessions which start before the range but end
2375 after are counted for the entire range.
2377 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2378 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2383 #note: POD here, implementation in FS::cust_svc
2384 sub seconds_since_sqlradacct {
2386 $self->cust_svc->seconds_since_sqlradacct(@_);
2389 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2391 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2392 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2393 TIMESTAMP_END (exclusive).
2395 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2396 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2401 #note: POD here, implementation in FS::cust_svc
2402 sub attribute_since_sqlradacct {
2404 $self->cust_svc->attribute_since_sqlradacct(@_);
2407 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2409 Returns an array of hash references of this customers login history for the
2410 given time range. (document this better)
2414 sub get_session_history {
2416 $self->cust_svc->get_session_history(@_);
2419 =item last_login_text
2421 Returns text describing the time of last login.
2425 sub last_login_text {
2427 $self->last_login ? ctime($self->last_login) : 'unknown';
2430 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2435 my($self, $start, $end, %opt ) = @_;
2437 my $did = $self->username; #yup
2439 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2441 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2443 #SELECT $for_update * FROM cdr
2444 # WHERE calldate >= $start #need a conversion
2445 # AND calldate < $end #ditto
2446 # AND ( charged_party = "$did"
2447 # OR charged_party = "$prefix$did" #if length($prefix);
2448 # OR ( ( charged_party IS NULL OR charged_party = '' )
2450 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2453 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2456 if ( length($prefix) ) {
2458 " AND ( charged_party = '$did'
2459 OR charged_party = '$prefix$did'
2460 OR ( ( charged_party IS NULL OR charged_party = '' )
2462 ( src = '$did' OR src = '$prefix$did' )
2468 " AND ( charged_party = '$did'
2469 OR ( ( charged_party IS NULL OR charged_party = '' )
2479 'select' => "$for_update *",
2482 #( freesidestatus IS NULL OR freesidestatus = '' )
2483 'freesidestatus' => '',
2485 'extra_sql' => $charged_or_src,
2493 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2499 if ( $self->usergroup ) {
2500 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2501 unless ref($self->usergroup) eq 'ARRAY';
2502 #when provisioning records, export callback runs in svc_Common.pm before
2503 #radius_usergroup records can be inserted...
2504 @{$self->usergroup};
2506 map { $_->groupname }
2507 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2511 =item clone_suspended
2513 Constructor used by FS::part_export::_export_suspend fallback. Document
2518 sub clone_suspended {
2520 my %hash = $self->hash;
2521 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2522 new FS::svc_acct \%hash;
2525 =item clone_kludge_unsuspend
2527 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2532 sub clone_kludge_unsuspend {
2534 my %hash = $self->hash;
2535 $hash{_password} = '';
2536 new FS::svc_acct \%hash;
2539 =item check_password
2541 Checks the supplied password against the (possibly encrypted) password in the
2542 database. Returns true for a successful authentication, false for no match.
2544 Currently supported encryptions are: classic DES crypt() and MD5
2548 sub check_password {
2549 my($self, $check_password) = @_;
2551 #remove old-style SUSPENDED kludge, they should be allowed to login to
2552 #self-service and pay up
2553 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2555 if ( $self->_password_encoding eq 'ldap' ) {
2557 my $auth = from_rfc2307 Authen::Passphrase $self->_password;
2558 return $auth->match($check_password);
2560 } elsif ( $self->_password_encoding eq 'crypt' ) {
2562 my $auth = from_crypt Authen::Passphrase $self->_password;
2563 return $auth->match($check_password);
2565 } elsif ( $self->_password_encoding eq 'plain' ) {
2567 return $check_password eq $password;
2571 #XXX this could be replaced with Authen::Passphrase stuff
2573 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2575 } elsif ( length($password) < 13 ) { #plaintext
2576 $check_password eq $password;
2577 } elsif ( length($password) == 13 ) { #traditional DES crypt
2578 crypt($check_password, $password) eq $password;
2579 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2580 unix_md5_crypt($check_password, $password) eq $password;
2581 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2582 warn "Can't check password: Blowfish encryption not yet supported, ".
2583 "svcnum ". $self->svcnum. "\n";
2586 warn "Can't check password: Unrecognized encryption for svcnum ".
2587 $self->svcnum. "\n";
2595 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2597 Returns an encrypted password, either by passing through an encrypted password
2598 in the database or by encrypting a plaintext password from the database.
2600 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2601 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2602 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2603 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2604 encryption type is only used if the password is not already encrypted in the
2609 sub crypt_password {
2612 if ( $self->_password_encoding eq 'ldap' ) {
2614 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2617 #XXX this could be replaced with Authen::Passphrase stuff
2619 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2620 if ( $encryption eq 'crypt' ) {
2623 $saltset[int(rand(64))].$saltset[int(rand(64))]
2625 } elsif ( $encryption eq 'md5' ) {
2626 unix_md5_crypt( $self->_password );
2627 } elsif ( $encryption eq 'blowfish' ) {
2628 croak "unknown encryption method $encryption";
2630 croak "unknown encryption method $encryption";
2633 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2637 } elsif ( $self->_password_encoding eq 'crypt' ) {
2639 return $self->_password;
2641 } elsif ( $self->_password_encoding eq 'plain' ) {
2643 #XXX this could be replaced with Authen::Passphrase stuff
2645 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2646 if ( $encryption eq 'crypt' ) {
2649 $saltset[int(rand(64))].$saltset[int(rand(64))]
2651 } elsif ( $encryption eq 'md5' ) {
2652 unix_md5_crypt( $self->_password );
2653 } elsif ( $encryption eq 'blowfish' ) {
2654 croak "unknown encryption method $encryption";
2656 croak "unknown encryption method $encryption";
2661 if ( length($self->_password) == 13
2662 || $self->_password =~ /^\$(1|2a?)\$/
2663 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2669 #XXX this could be replaced with Authen::Passphrase stuff
2671 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2672 if ( $encryption eq 'crypt' ) {
2675 $saltset[int(rand(64))].$saltset[int(rand(64))]
2677 } elsif ( $encryption eq 'md5' ) {
2678 unix_md5_crypt( $self->_password );
2679 } elsif ( $encryption eq 'blowfish' ) {
2680 croak "unknown encryption method $encryption";
2682 croak "unknown encryption method $encryption";
2691 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2693 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2694 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2695 "{MD5}5426824942db4253f87a1009fd5d2d4".
2697 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2698 to work the same as the B</crypt_password> method.
2704 #eventually should check a "password-encoding" field
2706 if ( $self->_password_encoding eq 'ldap' ) {
2708 return $self->_password;
2710 } elsif ( $self->_password_encoding eq 'crypt' ) {
2712 if ( length($self->_password) == 13 ) { #crypt
2713 return '{CRYPT}'. $self->_password;
2714 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2716 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2717 # die "Blowfish encryption not supported in this context, svcnum ".
2718 # $self->svcnum. "\n";
2720 warn "encryption method not (yet?) supported in LDAP context";
2721 return '{CRYPT}*'; #unsupported, should not auth
2724 } elsif ( $self->_password_encoding eq 'plain' ) {
2726 return '{PLAIN}'. $self->_password;
2728 #return '{CLEARTEXT}'. $self->_password; #?
2732 if ( length($self->_password) == 13 ) { #crypt
2733 return '{CRYPT}'. $self->_password;
2734 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2736 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2737 warn "Blowfish encryption not supported in this context, svcnum ".
2738 $self->svcnum. "\n";
2741 #are these two necessary anymore?
2742 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2743 return '{SSHA}'. $1;
2744 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2745 return '{NS-MTA-MD5}'. $1;
2748 return '{PLAIN}'. $self->_password;
2750 #return '{CLEARTEXT}'. $self->_password; #?
2752 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2753 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2754 #if ( $encryption eq 'crypt' ) {
2755 # return '{CRYPT}'. crypt(
2757 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2759 #} elsif ( $encryption eq 'md5' ) {
2760 # unix_md5_crypt( $self->_password );
2761 #} elsif ( $encryption eq 'blowfish' ) {
2762 # croak "unknown encryption method $encryption";
2764 # croak "unknown encryption method $encryption";
2772 =item domain_slash_username
2774 Returns $domain/$username/
2778 sub domain_slash_username {
2780 $self->domain. '/'. $self->username. '/';
2783 =item virtual_maildir
2785 Returns $domain/maildirs/$username/
2789 sub virtual_maildir {
2791 $self->domain. '/maildirs/'. $self->username. '/';
2796 =head1 CLASS METHODS
2800 =item search HASHREF
2802 Class method which returns a qsearch hash expression to search for parameters
2803 specified in HASHREF. Valid parameters are
2817 Arrayref of pkgparts
2823 Arrayref of additional WHERE clauses, will be ANDed together.
2834 my ($class, $params) = @_;
2839 if ( $params->{'domain'} ) {
2840 my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
2841 #preserve previous behavior & bubble up an error if $svc_domain not found?
2842 push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
2846 if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
2847 push @where, "domsvc = $1";
2851 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
2854 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
2855 push @where, "agentnum = $1";
2859 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
2860 push @where, "custnum = $1";
2864 if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
2865 #XXX untaint or sql quote
2867 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
2871 if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
2872 push @where, "popnum = $1";
2876 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
2877 push @where, "svcpart = $1";
2881 # here is the agent virtualization
2882 #if ($params->{CurrentUser}) {
2884 # qsearchs('access_user', { username => $params->{CurrentUser} });
2886 # if ($access_user) {
2887 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
2889 # push @where, "1=0";
2892 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
2893 'table' => 'cust_main',
2894 'null_right' => 'View/link unlinked services',
2898 push @where, @{ $params->{'where'} } if $params->{'where'};
2900 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
2902 my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
2903 ' LEFT JOIN part_svc USING ( svcpart ) '.
2904 ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
2905 ' LEFT JOIN cust_main USING ( custnum ) ';
2907 my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
2908 #if ( keys %svc_acct ) {
2909 # $count_query .= ' WHERE '.
2910 # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
2916 'table' => 'svc_acct',
2917 'hashref' => {}, # \%svc_acct,
2918 'select' => join(', ',
2921 'cust_main.custnum',
2922 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
2924 'addl_from' => $addl_from,
2925 'extra_sql' => $extra_sql,
2926 'order_by' => $params->{'order_by'},
2927 'count_query' => $count_query,
2940 This is the FS::svc_acct job-queue-able version. It still uses
2941 FS::Misc::send_email under-the-hood.
2948 eval "use FS::Misc qw(send_email)";
2951 $opt{mimetype} ||= 'text/plain';
2952 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2954 my $error = send_email(
2955 'from' => $opt{from},
2957 'subject' => $opt{subject},
2958 'content-type' => $opt{mimetype},
2959 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2961 die $error if $error;
2964 =item check_and_rebuild_fuzzyfiles
2968 sub check_and_rebuild_fuzzyfiles {
2969 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2970 -e "$dir/svc_acct.username"
2971 or &rebuild_fuzzyfiles;
2974 =item rebuild_fuzzyfiles
2978 sub rebuild_fuzzyfiles {
2980 use Fcntl qw(:flock);
2982 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2986 open(USERNAMELOCK,">>$dir/svc_acct.username")
2987 or die "can't open $dir/svc_acct.username: $!";
2988 flock(USERNAMELOCK,LOCK_EX)
2989 or die "can't lock $dir/svc_acct.username: $!";
2991 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2993 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2994 or die "can't open $dir/svc_acct.username.tmp: $!";
2995 print USERNAMECACHE join("\n", @all_username), "\n";
2996 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2998 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
3008 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
3009 open(USERNAMECACHE,"<$dir/svc_acct.username")
3010 or die "can't open $dir/svc_acct.username: $!";
3011 my @array = map { chomp; $_; } <USERNAMECACHE>;
3012 close USERNAMECACHE;
3016 =item append_fuzzyfiles USERNAME
3020 sub append_fuzzyfiles {
3021 my $username = shift;
3023 &check_and_rebuild_fuzzyfiles;
3025 use Fcntl qw(:flock);
3027 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
3029 open(USERNAME,">>$dir/svc_acct.username")
3030 or die "can't open $dir/svc_acct.username: $!";
3031 flock(USERNAME,LOCK_EX)
3032 or die "can't lock $dir/svc_acct.username: $!";
3034 print USERNAME "$username\n";
3036 flock(USERNAME,LOCK_UN)
3037 or die "can't unlock $dir/svc_acct.username: $!";
3045 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
3049 sub radius_usergroup_selector {
3050 my $sel_groups = shift;
3051 my %sel_groups = map { $_=>1 } @$sel_groups;
3053 my $selectname = shift || 'radius_usergroup';
3056 my $sth = $dbh->prepare(
3057 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
3058 ) or die $dbh->errstr;
3059 $sth->execute() or die $sth->errstr;
3060 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
3064 function ${selectname}_doadd(object) {
3065 var myvalue = object.${selectname}_add.value;
3066 var optionName = new Option(myvalue,myvalue,false,true);
3067 var length = object.$selectname.length;
3068 object.$selectname.options[length] = optionName;
3069 object.${selectname}_add.value = "";
3072 <SELECT MULTIPLE NAME="$selectname">
3075 foreach my $group ( @all_groups ) {
3076 $html .= qq(<OPTION VALUE="$group");
3077 if ( $sel_groups{$group} ) {
3078 $html .= ' SELECTED';
3079 $sel_groups{$group} = 0;
3081 $html .= ">$group</OPTION>\n";
3083 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
3084 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
3086 $html .= '</SELECT>';
3088 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
3089 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
3094 =item reached_threshold
3096 Performs some activities when svc_acct thresholds (such as number of seconds
3097 remaining) are reached.
3101 sub reached_threshold {
3104 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
3105 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
3107 if ( $opt{'op'} eq '+' ){
3108 $svc_acct->setfield( $opt{'column'}.'_threshold',
3109 int($svc_acct->getfield($opt{'column'})
3110 * ( $conf->exists('svc_acct-usage_threshold')
3111 ? $conf->config('svc_acct-usage_threshold')/100
3116 my $error = $svc_acct->replace;
3117 die $error if $error;
3118 }elsif ( $opt{'op'} eq '-' ){
3120 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
3121 return '' if ($threshold eq '' );
3123 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
3124 my $error = $svc_acct->replace;
3125 die $error if $error; # email next time, i guess
3127 if ( $warning_template ) {
3128 eval "use FS::Misc qw(send_email)";
3131 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
3132 my $cust_main = $cust_pkg->cust_main;
3134 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
3135 $cust_main->invoicing_list,
3136 ($opt{'to'} ? $opt{'to'} : ())
3139 my $mimetype = $warning_mimetype;
3140 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
3142 my $body = $warning_template->fill_in( HASH => {
3143 'custnum' => $cust_main->custnum,
3144 'username' => $svc_acct->username,
3145 'password' => $svc_acct->_password,
3146 'first' => $cust_main->first,
3147 'last' => $cust_main->getfield('last'),
3148 'pkg' => $cust_pkg->part_pkg->pkg,
3149 'column' => $opt{'column'},
3150 'amount' => $opt{'column'} =~/bytes/
3151 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
3152 : $svc_acct->getfield($opt{'column'}),
3153 'threshold' => $opt{'column'} =~/bytes/
3154 ? FS::UI::bytecount::display_bytecount($threshold)
3159 my $error = send_email(
3160 'from' => $warning_from,
3162 'subject' => $warning_subject,
3163 'content-type' => $mimetype,
3164 'body' => [ map "$_\n", split("\n", $body) ],
3166 die $error if $error;
3169 die "unknown op: " . $opt{'op'};
3177 The $recref stuff in sub check should be cleaned up.
3179 The suspend, unsuspend and cancel methods update the database, but not the
3180 current object. This is probably a bug as it's unexpected and
3183 radius_usergroup_selector? putting web ui components in here? they should
3184 probably live somewhere else...
3186 insertion of RADIUS group stuff in insert could be done with child_objects now
3187 (would probably clean up export of them too)
3189 _op_usage and set_usage bypass the history... maybe they shouldn't
3193 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
3194 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
3195 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
3196 L<freeside-queued>), L<FS::svc_acct_pop>,
3197 schema.html from the base documentation.