4 use base qw( FS::svc_Domain_Mixin
15 use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
16 $dir_prefix @shells $usernamemin
17 $usernamemax $passwordmin $passwordmax
18 $username_ampersand $username_letter $username_letterfirst
19 $username_noperiod $username_nounderscore $username_nodash
20 $username_uppercase $username_percent $username_colon
21 $username_slash $username_equals $username_pound
23 $password_noampersand $password_noexclamation
24 $warning_template $warning_from $warning_subject $warning_mimetype
27 $radius_password $radius_ip
30 use Scalar::Util qw( blessed );
35 use Crypt::PasswdMD5 1.2;
36 use Digest::SHA 'sha1_base64';
37 use Digest::MD5 'md5_base64';
40 use Authen::Passphrase;
41 use FS::UID qw( datasrc driver_name );
43 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
44 use FS::Msgcat qw(gettext);
45 use FS::UI::bytecount;
47 use FS::PagedSearch qw( psearch ); # XXX in v4, replace with FS::Cursor
51 use FS::cust_main_invoice;
56 use FS::radius_usergroup;
67 $me = '[FS::svc_acct]';
69 #ask FS::UID to run this stuff for us later
70 FS::UID->install_callback( sub {
72 $dir_prefix = $conf->config('home');
73 @shells = $conf->config('shells');
74 $usernamemin = $conf->config('usernamemin') || 2;
75 $usernamemax = $conf->config('usernamemax');
76 $passwordmin = $conf->config('passwordmin'); # || 6;
78 $passwordmin = ( defined($passwordmin) && $passwordmin =~ /\d+/ )
81 $passwordmax = $conf->config('passwordmax') || 8;
82 $username_letter = $conf->exists('username-letter');
83 $username_letterfirst = $conf->exists('username-letterfirst');
84 $username_noperiod = $conf->exists('username-noperiod');
85 $username_nounderscore = $conf->exists('username-nounderscore');
86 $username_nodash = $conf->exists('username-nodash');
87 $username_uppercase = $conf->exists('username-uppercase');
88 $username_ampersand = $conf->exists('username-ampersand');
89 $username_percent = $conf->exists('username-percent');
90 $username_colon = $conf->exists('username-colon');
91 $username_slash = $conf->exists('username-slash');
92 $username_equals = $conf->exists('username-equals');
93 $username_pound = $conf->exists('username-pound');
94 $username_exclamation = $conf->exists('username-exclamation');
95 $password_noampersand = $conf->exists('password-noexclamation');
96 $password_noexclamation = $conf->exists('password-noexclamation');
97 $dirhash = $conf->config('dirhash') || 0;
98 if ( $conf->exists('warning_email') ) {
99 $warning_template = new Text::Template (
101 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
102 ) or warn "can't create warning email template: $Text::Template::ERROR";
103 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
104 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
105 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
106 $warning_cc = $conf->config('warning_email-cc');
108 $warning_template = '';
110 $warning_subject = '';
111 $warning_mimetype = '';
114 $smtpmachine = $conf->config('smtpmachine');
115 $radius_password = $conf->config('radius-password') || 'Password';
116 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
117 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
121 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
122 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '.', ',' );
126 my ( $hashref, $cache ) = @_;
127 if ( $hashref->{'svc_acct_svcnum'} ) {
128 $self->{'_domsvc'} = FS::svc_domain->new( {
129 'svcnum' => $hashref->{'domsvc'},
130 'domain' => $hashref->{'svc_acct_domain'},
131 'catchall' => $hashref->{'svc_acct_catchall'},
138 FS::svc_acct - Object methods for svc_acct records
144 $record = new FS::svc_acct \%hash;
145 $record = new FS::svc_acct { 'column' => 'value' };
147 $error = $record->insert;
149 $error = $new_record->replace($old_record);
151 $error = $record->delete;
153 $error = $record->check;
155 $error = $record->suspend;
157 $error = $record->unsuspend;
159 $error = $record->cancel;
161 %hash = $record->radius;
163 %hash = $record->radius_reply;
165 %hash = $record->radius_check;
167 $domain = $record->domain;
169 $svc_domain = $record->svc_domain;
171 $email = $record->email;
173 $seconds_since = $record->seconds_since($timestamp);
177 An FS::svc_acct object represents an account. FS::svc_acct inherits from
178 FS::svc_Common. The following fields are currently supported:
184 Primary key (assigned automatcially for new accounts)
192 =item _password_encoding
194 plain, crypt, ldap (or empty for autodetection)
202 Point of presence (see L<FS::svc_acct_pop>)
214 set automatically if blank (and uid is not)
234 svcnum from svc_domain
238 Optional svcnum from svc_pbx
240 =item radius_I<Radius_Attribute>
242 I<Radius-Attribute> (reply)
244 =item rc_I<Radius_Attribute>
246 I<Radius-Attribute> (check)
256 Creates a new account. To add the account to the database, see L<"insert">.
263 'longname_plural' => 'Access accounts and mailboxes',
264 'sorts' => [ 'username', 'uid', 'seconds', 'last_login' ],
265 'display_weight' => 10,
266 'cancel_weight' => 50,
267 'ip_field' => 'slipip',
268 'manual_require' => 1,
270 'dir' => 'Home directory',
273 def_info => 'set to fixed and blank for no UIDs',
276 'slipip' => 'IP address',
277 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
279 label => 'Access number',
281 select_table => 'svc_acct_pop',
282 select_key => 'popnum',
283 select_label => 'city',
289 disable_default => 1,
294 'password_selfchange' => { label => 'Password modification',
297 'password_recover' => { label => 'Password recovery',
301 label => 'Quota', #Mail storage limit
303 disable_inventory => 1,
306 label => 'File storage limit',
308 disable_inventory => 1,
311 label => 'Number of files limit',
313 disable_inventory => 1,
316 label => 'File size limit',
318 disable_inventory => 1,
320 '_password' => { label => 'Password',
325 def_info => 'when blank, defaults to UID',
330 def_info => 'set to blank for no shell tracking',
332 #select_list => [ $conf->config('shells') ],
333 select_list => [ $conf ? $conf->config('shells') : () ],
334 disable_inventory => 1,
337 'finger' => 'Real name', # (GECOS)',
341 select_table => 'svc_domain',
342 select_key => 'svcnum',
343 select_label => 'domain',
344 disable_inventory => 1,
347 'pbxsvc' => { label => 'PBX',
348 type => 'select-svc_pbx.html',
349 disable_inventory => 1,
350 disable_select => 1, #UI wonky, pry works otherwise
352 'sectornum' => 'Tower sector',
354 label => 'RADIUS groups',
355 type => 'select-radius_group.html',
356 disable_inventory => 1,
360 'seconds' => { label => 'Seconds',
361 label_sort => 'with Time Remaining',
363 disable_inventory => 1,
365 disable_part_svc_column => 1,
367 'upbytes' => { label => 'Upload',
369 disable_inventory => 1,
371 'format' => \&FS::UI::bytecount::display_bytecount,
372 'parse' => \&FS::UI::bytecount::parse_bytecount,
373 disable_part_svc_column => 1,
375 'downbytes' => { label => 'Download',
377 disable_inventory => 1,
379 'format' => \&FS::UI::bytecount::display_bytecount,
380 'parse' => \&FS::UI::bytecount::parse_bytecount,
381 disable_part_svc_column => 1,
383 'totalbytes'=> { label => 'Total up and download',
385 disable_inventory => 1,
387 'format' => \&FS::UI::bytecount::display_bytecount,
388 'parse' => \&FS::UI::bytecount::parse_bytecount,
389 disable_part_svc_column => 1,
391 'seconds_threshold' => { label => 'Seconds threshold',
393 disable_inventory => 1,
395 disable_part_svc_column => 1,
397 'upbytes_threshold' => { label => 'Upload threshold',
399 disable_inventory => 1,
401 'format' => \&FS::UI::bytecount::display_bytecount,
402 'parse' => \&FS::UI::bytecount::parse_bytecount,
403 disable_part_svc_column => 1,
405 'downbytes_threshold' => { label => 'Download threshold',
407 disable_inventory => 1,
409 'format' => \&FS::UI::bytecount::display_bytecount,
410 'parse' => \&FS::UI::bytecount::parse_bytecount,
411 disable_part_svc_column => 1,
413 'totalbytes_threshold'=> { label => 'Total up and download threshold',
415 disable_inventory => 1,
417 'format' => \&FS::UI::bytecount::display_bytecount,
418 'parse' => \&FS::UI::bytecount::parse_bytecount,
419 disable_part_svc_column => 1,
422 label => 'Last login',
426 label => 'Last logout',
431 label => 'Communigate aliases',
433 disable_inventory => 1,
438 label => 'Communigate account type',
440 select_list => [qw( MultiMailbox TextMailbox MailDirMailbox AGrade BGrade CGrade )],
441 disable_inventory => 1,
444 'cgp_accessmodes' => {
445 label => 'Communigate enabled services',
446 type => 'communigate_pro-accessmodes',
447 disable_inventory => 1,
450 'cgp_rulesallowed' => {
451 label => 'Allowed mail rules',
453 select_list => [ '', 'No', 'Filter Only', 'All But Exec', 'Any' ],
454 disable_inventory => 1,
457 'cgp_rpopallowed' => { label => 'RPOP modifications',
460 'cgp_mailtoall' => { label => 'Accepts mail to "all"',
463 'cgp_addmailtrailer' => { label => 'Add trailer to sent mail',
466 'cgp_archiveafter' => {
467 label => 'Archive messages after',
470 -2 => 'default(730 days)',
477 1209600 => '2 weeks',
478 2592000 => '30 days',
479 7776000 => '90 days',
480 15552000 => '180 days',
481 31536000 => '365 days',
482 63072000 => '730 days',
484 disable_inventory => 1,
490 'cgp_deletemode' => {
491 label => 'Communigate message delete method',
493 select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
494 disable_inventory => 1,
497 'cgp_emptytrash' => {
498 label => 'Communigate on logout remove trash',
500 select_list => __PACKAGE__->cgp_emptytrash_values,
501 disable_inventory => 1,
505 label => 'Communigate language',
507 select_list => [ '', qw( English Arabic Chinese Dutch French German Hebrew Italian Japanese Portuguese Russian Slovak Spanish Thai ) ],
508 disable_inventory => 1,
512 label => 'Communigate time zone',
514 select_list => __PACKAGE__->cgp_timezone_values,
515 disable_inventory => 1,
519 label => 'Communigate layout',
521 select_list => [ '', '***', 'GoldFleece', 'Skin2' ],
522 disable_inventory => 1,
525 'cgp_prontoskinname' => {
526 label => 'Communigate Pronto style',
528 select_list => [ '', 'Pronto', 'Pronto-darkflame', 'Pronto-steel', 'Pronto-twilight', ],
529 disable_inventory => 1,
532 'cgp_sendmdnmode' => {
533 label => 'Communigate send read receipts',
535 select_list => [ '', 'Never', 'Manually', 'Automatically' ],
536 disable_inventory => 1,
547 sub table { 'svc_acct'; }
549 sub table_dupcheck_fields { ( 'username', 'domsvc' ); }
552 shift->_lastlog('in', @_);
556 shift->_lastlog('out', @_);
560 my( $self, $op, $time ) = @_;
562 if ( defined($time) ) {
563 warn "$me last_log$op called on svcnum ". $self->svcnum.
564 ' ('. $self->email. "): $time\n"
569 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
573 my $sth = $dbh->prepare( $sql )
574 or die "Error preparing $sql: ". $dbh->errstr;
575 my $rv = $sth->execute($time, $self->svcnum);
576 die "Error executing $sql: ". $sth->errstr
578 die "Can't update last_log$op for svcnum". $self->svcnum
581 $self->{'Hash'}->{"last_log$op"} = $time;
583 $self->getfield("last_log$op");
587 =item search_sql STRING
589 Class method which returns an SQL fragment to search for the given string.
594 my( $class, $string ) = @_;
595 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
596 my( $username, $domain ) = ( $1, $2 );
597 my $q_username = dbh->quote($username);
598 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
600 "svc_acct.username = $q_username AND ( ".
601 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
606 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
608 $class->search_sql_field('slipip', $string ).
610 $class->search_sql_field('username', $string ).
613 $class->search_sql_field('username', $string);
617 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
619 Returns the "username@domain" string for this account.
621 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
631 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
633 Returns a longer string label for this acccount ("Real Name <username@domain>"
634 if available, or "username@domain").
636 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
643 my $label = $self->label(@_);
644 my $finger = $self->finger;
645 return $label unless $finger =~ /\S/;
646 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
647 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
651 =item insert [ , OPTION => VALUE ... ]
653 Adds this account to the database. If there is an error, returns the error,
654 otherwise returns false.
656 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
657 defined. An FS::cust_svc record will be created and inserted.
659 The additional field I<usergroup> can optionally be defined; if so it should
660 contain an arrayref of group names. See L<FS::radius_usergroup>.
662 The additional field I<child_objects> can optionally be defined; if so it
663 should contain an arrayref of FS::tablename objects. They will have their
664 svcnum fields set and will be inserted after this record, but before any
665 exports are run. Each element of the array can also optionally be a
666 two-element array reference containing the child object and the name of an
667 alternate field to be filled in with the newly-inserted svcnum, for example
668 C<[ $svc_forward, 'srcsvc' ]>
670 Currently available options are: I<depend_jobnum>
672 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
673 jobnums), all provisioning jobs will have a dependancy on the supplied
674 jobnum(s) (they will not run until the specific job(s) complete(s)).
676 (TODOC: L<FS::queue> and L<freeside-queued>)
678 (TODOC: new exports!)
687 warn "[$me] insert called on $self: ". Dumper($self).
688 "\nwith options: ". Dumper(%options);
691 local $SIG{HUP} = 'IGNORE';
692 local $SIG{INT} = 'IGNORE';
693 local $SIG{QUIT} = 'IGNORE';
694 local $SIG{TERM} = 'IGNORE';
695 local $SIG{TSTP} = 'IGNORE';
696 local $SIG{PIPE} = 'IGNORE';
698 my $oldAutoCommit = $FS::UID::AutoCommit;
699 local $FS::UID::AutoCommit = 0;
703 my $error = $self->SUPER::insert( # usergroup is here
704 'jobnums' => \@jobnums,
705 'child_objects' => $self->child_objects,
709 $error ||= $self->insert_password_history;
712 $dbh->rollback if $oldAutoCommit;
716 unless ( $skip_fuzzyfiles ) {
717 $error = $self->queue_fuzzyfiles_update;
719 $dbh->rollback if $oldAutoCommit;
720 return "updating fuzzy search cache: $error";
724 my $cust_pkg = $self->cust_svc->cust_pkg;
727 my $cust_main = $cust_pkg->cust_main;
728 my $agentnum = $cust_main->agentnum;
730 if ( $conf->exists('emailinvoiceautoalways')
731 || $conf->exists('emailinvoiceauto')
732 && ! $cust_main->invoicing_list_emailonly
734 my @invoicing_list = $cust_main->invoicing_list;
735 push @invoicing_list, $self->email;
736 $cust_main->invoicing_list(\@invoicing_list);
740 my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude');
741 unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) {
743 my $msgnum = $conf->config('welcome_msgnum', $agentnum);
745 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
746 $error = $msg_template->send('cust_main' => $cust_main,
750 my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
751 = ('','','','','','');
753 if ( $conf->exists('welcome_email', $agentnum) ) {
754 $welcome_template = new Text::Template (
756 SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
757 ) or warn "can't create welcome email template: $Text::Template::ERROR";
758 $welcome_from = $conf->config('welcome_email-from', $agentnum);
759 # || 'your-isp-is-dum'
760 $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
762 $welcome_subject_template = new Text::Template (
764 SOURCE => $welcome_subject,
765 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
766 $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
769 if ( $welcome_template ) {
770 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
774 'custnum' => $self->custnum,
775 'username' => $self->username,
776 'password' => $self->_password,
777 'first' => $cust_main->first,
778 'last' => $cust_main->getfield('last'),
779 'pkg' => $cust_pkg->part_pkg->pkg,
781 my $wqueue = new FS::queue {
782 'svcnum' => $self->svcnum,
783 'job' => 'FS::svc_acct::send_email'
785 my $error = $wqueue->insert(
787 'from' => $welcome_from,
788 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
789 'mimetype' => $welcome_mimetype,
790 'body' => $welcome_template->fill_in( HASH => \%hash, ),
793 $dbh->rollback if $oldAutoCommit;
794 return "error queuing welcome email: $error";
797 if ( $options{'depend_jobnum'} ) {
798 warn "$me depend_jobnum found; adding to welcome email dependancies"
800 if ( ref($options{'depend_jobnum'}) ) {
801 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
802 "to welcome email dependancies"
804 push @jobnums, @{ $options{'depend_jobnum'} };
806 warn "$me adding job $options{'depend_jobnum'} ".
807 "to welcome email dependancies"
809 push @jobnums, $options{'depend_jobnum'};
813 foreach my $jobnum ( @jobnums ) {
814 my $error = $wqueue->depend_insert($jobnum);
816 $dbh->rollback if $oldAutoCommit;
817 return "error queuing welcome email job dependancy: $error";
823 } # if $welcome_template
828 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
832 # set usage fields and thresholds if unset but set in a package def
833 # AND the package already has a last bill date (otherwise they get double added)
834 sub preinsert_hook_first {
837 return '' unless $self->pkgnum;
839 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
840 return '' unless $cust_pkg && $cust_pkg->last_bill;
842 my $part_pkg = $cust_pkg->part_pkg;
843 return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
845 my %values = $part_pkg->usage_valuehash;
846 my $multiplier = $conf->exists('svc_acct-usage_threshold')
847 ? 1 - $conf->config('svc_acct-usage_threshold')/100
848 : 0.20; #doesn't matter
850 foreach ( keys %values ) {
851 next if $self->getfield($_);
852 $self->setfield( $_, $values{$_} );
853 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
854 if $conf->exists('svc_acct-usage_threshold');
862 Deletes this account from the database. If there is an error, returns the
863 error, otherwise returns false.
865 The corresponding FS::cust_svc record will be deleted as well.
867 (TODOC: new exports!)
874 return "can't delete system account" if $self->_check_system;
876 return "Can't delete an account which is a (svc_forward) source!"
877 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
879 return "Can't delete an account which is a (svc_forward) destination!"
880 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
882 return "Can't delete an account with (svc_www) web service!"
883 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
885 # what about records in session ? (they should refer to history table)
887 local $SIG{HUP} = 'IGNORE';
888 local $SIG{INT} = 'IGNORE';
889 local $SIG{QUIT} = 'IGNORE';
890 local $SIG{TERM} = 'IGNORE';
891 local $SIG{TSTP} = 'IGNORE';
892 local $SIG{PIPE} = 'IGNORE';
894 my $oldAutoCommit = $FS::UID::AutoCommit;
895 local $FS::UID::AutoCommit = 0;
898 foreach my $cust_main_invoice (
899 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
901 unless ( defined($cust_main_invoice) ) {
902 warn "WARNING: something's wrong with qsearch";
905 my %hash = $cust_main_invoice->hash;
906 $hash{'dest'} = $self->email;
907 my $new = new FS::cust_main_invoice \%hash;
908 my $error = $new->replace($cust_main_invoice);
910 $dbh->rollback if $oldAutoCommit;
915 foreach my $svc_domain (
916 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
918 my %hash = new FS::svc_domain->hash;
919 $hash{'catchall'} = '';
920 my $new = new FS::svc_domain \%hash;
921 my $error = $new->replace($svc_domain);
923 $dbh->rollback if $oldAutoCommit;
928 my $error = $self->delete_password_history
929 || $self->SUPER::delete; # usergroup here
931 $dbh->rollback if $oldAutoCommit;
935 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
939 =item replace OLD_RECORD
941 Replaces OLD_RECORD with this one in the database. If there is an error,
942 returns the error, otherwise returns false.
944 The additional field I<usergroup> can optionally be defined; if so it should
945 contain an arrayref of group names. See L<FS::radius_usergroup>.
953 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
957 warn "$me replacing $old with $new\n" if $DEBUG;
961 return "can't modify system account" if $old->_check_system;
964 #no warnings 'numeric'; #alas, a 5.006-ism
967 foreach my $xid (qw( uid gid )) {
969 return "Can't change $xid!"
970 if ! $conf->exists("svc_acct-edit_$xid")
971 && $old->$xid() != $new->$xid()
972 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
977 return "can't change username"
978 if $old->username ne $new->username
979 && $conf->exists('svc_acct-no_edit_username');
981 #change homdir when we change username
982 $new->setfield('dir', '') if $old->username ne $new->username;
984 local $SIG{HUP} = 'IGNORE';
985 local $SIG{INT} = 'IGNORE';
986 local $SIG{QUIT} = 'IGNORE';
987 local $SIG{TERM} = 'IGNORE';
988 local $SIG{TSTP} = 'IGNORE';
989 local $SIG{PIPE} = 'IGNORE';
991 my $oldAutoCommit = $FS::UID::AutoCommit;
992 local $FS::UID::AutoCommit = 0;
995 $error = $new->SUPER::replace($old, @_); # usergroup here
997 # don't need to record this unless the password was changed
998 if ( $old->_password ne $new->_password ) {
999 $error ||= $new->insert_password_history;
1003 $dbh->rollback if $oldAutoCommit;
1004 return $error if $error;
1007 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
1008 $error = $new->queue_fuzzyfiles_update;
1010 $dbh->rollback if $oldAutoCommit;
1011 return "updating fuzzy search cache: $error";
1015 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1019 =item queue_fuzzyfiles_update
1021 Used by insert & replace to update the fuzzy search cache
1025 sub queue_fuzzyfiles_update {
1028 local $SIG{HUP} = 'IGNORE';
1029 local $SIG{INT} = 'IGNORE';
1030 local $SIG{QUIT} = 'IGNORE';
1031 local $SIG{TERM} = 'IGNORE';
1032 local $SIG{TSTP} = 'IGNORE';
1033 local $SIG{PIPE} = 'IGNORE';
1035 my $oldAutoCommit = $FS::UID::AutoCommit;
1036 local $FS::UID::AutoCommit = 0;
1039 my $queue = new FS::queue {
1040 'svcnum' => $self->svcnum,
1041 'job' => 'FS::svc_acct::append_fuzzyfiles'
1043 my $error = $queue->insert($self->username);
1045 $dbh->rollback if $oldAutoCommit;
1046 return "queueing job (transaction rolled back): $error";
1049 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1057 Suspends this account by calling export-specific suspend hooks. If there is
1058 an error, returns the error, otherwise returns false.
1060 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1066 return "can't suspend system account" if $self->_check_system;
1067 $self->SUPER::suspend(@_);
1072 Unsuspends this account by by calling export-specific suspend hooks. If there
1073 is an error, returns the error, otherwise returns false.
1075 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
1081 my %hash = $self->hash;
1082 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
1083 $hash{_password} = $1;
1084 my $new = new FS::svc_acct ( \%hash );
1085 my $error = $new->replace($self);
1086 return $error if $error;
1089 $self->SUPER::unsuspend(@_);
1094 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1096 If the B<auto_unset_catchall> configuration option is set, this method will
1097 automatically remove any references to the canceled service in the catchall
1098 field of svc_domain. This allows packages that contain both a svc_domain and
1099 its catchall svc_acct to be canceled in one step.
1104 # Only one thing to do at this level
1106 foreach my $svc_domain (
1107 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
1108 if($conf->exists('auto_unset_catchall')) {
1109 my %hash = $svc_domain->hash;
1110 $hash{catchall} = '';
1111 my $new = new FS::svc_domain ( \%hash );
1112 my $error = $new->replace($svc_domain);
1113 return $error if $error;
1115 return "cannot unprovision svc_acct #".$self->svcnum.
1116 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
1120 $self->SUPER::cancel(@_);
1126 Checks all fields to make sure this is a valid service. If there is an error,
1127 returns the error, otherwise returns false. Called by the insert and replace
1130 Sets any fixed values; see L<FS::part_svc>.
1137 my($recref) = $self->hashref;
1139 my $x = $self->setfixed;
1140 return $x unless ref($x);
1143 my $error = $self->ut_numbern('svcnum')
1144 #|| $self->ut_number('domsvc')
1145 || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
1146 || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
1147 || $self->ut_foreign_keyn('sectornum','tower_sector','sectornum')
1148 || $self->ut_foreign_keyn('routernum','router','routernum')
1149 || $self->ut_foreign_keyn('blocknum','addr_block','blocknum')
1150 || $self->ut_textn('sec_phrase')
1151 || $self->ut_snumbern('seconds')
1152 || $self->ut_snumbern('upbytes')
1153 || $self->ut_snumbern('downbytes')
1154 || $self->ut_snumbern('totalbytes')
1155 || $self->ut_snumbern('seconds_threshold')
1156 || $self->ut_snumbern('upbytes_threshold')
1157 || $self->ut_snumbern('downbytes_threshold')
1158 || $self->ut_snumbern('totalbytes_threshold')
1159 || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
1160 || $self->ut_enum('password_selfchange', [ '', 'Y' ])
1161 || $self->ut_enum('password_recover', [ '', 'Y' ])
1163 || $self->ut_anything('cf_privatekey')
1165 || $self->ut_textn('cgp_accessmodes')
1166 || $self->ut_alphan('cgp_type')
1167 || $self->ut_textn('cgp_aliases' ) #well
1169 || $self->ut_alphasn('cgp_rulesallowed')
1170 || $self->ut_enum('cgp_rpopallowed', [ '', 'Y' ])
1171 || $self->ut_enum('cgp_mailtoall', [ '', 'Y' ])
1172 || $self->ut_enum('cgp_addmailtrailer', [ '', 'Y' ])
1173 || $self->ut_snumbern('cgp_archiveafter')
1175 || $self->ut_alphasn('cgp_deletemode')
1176 || $self->ut_enum('cgp_emptytrash', $self->cgp_emptytrash_values)
1177 || $self->ut_alphan('cgp_language')
1178 || $self->ut_textn('cgp_timezone')
1179 || $self->ut_textn('cgp_skinname')
1180 || $self->ut_textn('cgp_prontoskinname')
1181 || $self->ut_alphan('cgp_sendmdnmode')
1183 return $error if $error;
1185 # assign IP address, etc.
1186 if ( $conf->exists('svc_acct-ip_addr') ) {
1187 my $error = $self->svc_ip_check;
1188 return $error if $error;
1189 } else { # I think this is correct
1190 $self->routernum('');
1191 $self->blocknum('');
1195 local $username_letter = $username_letter;
1196 local $username_uppercase = $username_uppercase;
1197 if ($self->svcnum) {
1198 my $cust_svc = $self->cust_svc
1199 or return "no cust_svc record found for svcnum ". $self->svcnum;
1200 my $cust_pkg = $cust_svc->cust_pkg;
1202 if ($self->pkgnum) {
1203 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );#complain?
1207 $conf->exists('username-letter', $cust_pkg->cust_main->agentnum);
1208 $username_uppercase =
1209 $conf->exists('username-uppercase', $cust_pkg->cust_main->agentnum);
1212 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1214 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#\!]{$usernamemin,$ulen})$/i
1215 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1216 $recref->{username} = $1;
1218 my $uerror = gettext('illegal_username'). ': '. $recref->{username};
1220 unless ( $username_uppercase ) {
1221 $recref->{username} =~ /[A-Z]/ and return $uerror;
1223 if ( $username_letterfirst ) {
1224 $recref->{username} =~ /^[a-z]/ or return $uerror;
1225 } elsif ( $username_letter ) {
1226 $recref->{username} =~ /[a-z]/ or return $uerror;
1228 if ( $username_noperiod ) {
1229 $recref->{username} =~ /\./ and return $uerror;
1231 if ( $username_nounderscore ) {
1232 $recref->{username} =~ /_/ and return $uerror;
1234 if ( $username_nodash ) {
1235 $recref->{username} =~ /\-/ and return $uerror;
1237 unless ( $username_ampersand ) {
1238 $recref->{username} =~ /\&/ and return $uerror;
1240 unless ( $username_percent ) {
1241 $recref->{username} =~ /\%/ and return $uerror;
1243 unless ( $username_colon ) {
1244 $recref->{username} =~ /\:/ and return $uerror;
1246 unless ( $username_slash ) {
1247 $recref->{username} =~ /\// and return $uerror;
1249 unless ( $username_equals ) {
1250 $recref->{username} =~ /\=/ and return $uerror;
1252 unless ( $username_pound ) {
1253 $recref->{username} =~ /\#/ and return $uerror;
1255 unless ( $username_exclamation ) {
1256 $recref->{username} =~ /\!/ and return $uerror;
1260 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1261 $recref->{popnum} = $1;
1262 return "Unknown popnum" unless
1263 ! $recref->{popnum} ||
1264 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1266 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1268 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1269 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1271 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1272 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1273 #not all systems use gid=uid
1274 #you can set a fixed gid in part_svc
1276 return "Only root can have uid 0"
1277 if $recref->{uid} == 0
1278 && $recref->{username} !~ /^(root|toor|smtp)$/;
1280 unless ( $recref->{username} eq 'sync' ) {
1281 if ( grep $_ eq $recref->{shell}, @shells ) {
1282 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1284 return "Illegal shell \`". $self->shell. "\'; ".
1285 "shells configuration value contains: @shells";
1288 $recref->{shell} = '/bin/sync';
1292 $recref->{gid} ne '' ?
1293 return "Can't have gid without uid" : ( $recref->{gid}='' );
1294 #$recref->{dir} ne '' ?
1295 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1296 $recref->{shell} ne '' ?
1297 return "Can't have shell without uid" : ( $recref->{shell}='' );
1300 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1302 $recref->{dir} =~ /^([\/\w\-\.\&\:\#]*)$/
1303 or return "Illegal directory: ". $recref->{dir};
1304 $recref->{dir} = $1;
1305 return "Illegal directory"
1306 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1307 return "Illegal directory"
1308 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1309 unless ( $recref->{dir} ) {
1310 $recref->{dir} = $dir_prefix . '/';
1311 if ( $dirhash > 0 ) {
1312 for my $h ( 1 .. $dirhash ) {
1313 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1315 } elsif ( $dirhash < 0 ) {
1316 for my $h ( reverse $dirhash .. -1 ) {
1317 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1320 $recref->{dir} .= $recref->{username};
1326 if ( $self->getfield('finger') eq '' ) {
1327 my $cust_pkg = $self->svcnum
1328 ? $self->cust_svc->cust_pkg
1329 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1331 my $cust_main = $cust_pkg->cust_main;
1332 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1335 # $error = $self->ut_textn('finger');
1336 # return $error if $error;
1337 $self->getfield('finger') =~ /^([\w \,\.\-\'\&\t\!\@\#\$\%\(\)\+\;\"\?\/\*\<\>]*)$/
1338 or return "Illegal finger: ". $self->getfield('finger');
1339 $self->setfield('finger', $1);
1341 for (qw( quota file_quota file_maxsize )) {
1342 $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
1345 $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
1346 $recref->{file_maxnum} = $1;
1348 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1349 if ( $recref->{slipip} eq '' ) {
1350 $recref->{slipip} = ''; # eh?
1351 } elsif ( $recref->{slipip} eq '0e0' ) {
1352 $recref->{slipip} = '0e0';
1354 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1355 or return "Illegal slipip: ". $self->slipip;
1356 $recref->{slipip} = $1;
1360 #arbitrary RADIUS stuff; allow ut_textn for now
1361 foreach ( grep /^radius_/, fields('svc_acct') ) {
1362 $self->ut_textn($_);
1365 # First, if _password is blank, generate one and set default encoding.
1366 if ( ! $recref->{_password} ) {
1367 $error = $self->set_password('');
1369 # But if there's a _password but no encoding, assume it's plaintext and
1370 # set it to default encoding.
1371 elsif ( ! $recref->{_password_encoding} ) {
1372 $error = $self->set_password($recref->{_password});
1374 return $error if $error;
1376 # Next, check _password to ensure compliance with the encoding.
1377 if ( $recref->{_password_encoding} eq 'ldap' ) {
1379 if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
1380 $recref->{_password} = uc($1).$2;
1382 return 'Illegal (ldap-encoded) password: '. $recref->{_password};
1385 } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
1387 if ( $recref->{_password} =~
1388 #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
1389 /^(!!?)?(\$\w+\$.*|[\w\+\/\.]{13}|_[\w\+\/\.]{19}|\*)$/
1392 $recref->{_password} = ( defined($1) ? $1 : '' ). $2;
1395 return 'Illegal (crypt-encoded) password: '. $recref->{_password};
1398 } elsif ( $recref->{_password_encoding} eq 'plain' ) {
1399 # Password randomization is now in set_password.
1400 # Strip whitespace characters, check length requirements, etc.
1401 if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1402 $recref->{_password} = $1;
1404 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1405 FS::Msgcat::_gettext('illegal_password_characters').
1406 ": ". $recref->{_password};
1409 if ( $password_noampersand ) {
1410 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1412 if ( $password_noexclamation ) {
1413 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1417 return "invalid password encoding ('".$recref->{_password_encoding}."'";
1420 $self->SUPER::check;
1425 sub _password_encryption {
1427 my $encoding = lc($self->_password_encoding);
1428 return if !$encoding;
1429 return 'plain' if $encoding eq 'plain';
1430 if($encoding eq 'crypt') {
1431 my $pass = $self->_password;
1432 $pass =~ s/^\*SUSPENDED\* //;
1434 return 'md5' if $pass =~ /^\$1\$/;
1435 #return 'blowfish' if $self->_password =~ /^\$2\$/;
1436 return 'des' if length($pass) == 13;
1439 if($encoding eq 'ldap') {
1440 uc($self->_password) =~ /^\{([\w-]+)\}/;
1441 return 'crypt' if $1 eq 'CRYPT' or $1 eq 'DES';
1442 return 'plain' if $1 eq 'PLAIN' or $1 eq 'CLEARTEXT';
1443 return 'md5' if $1 eq 'MD5';
1444 return 'sha1' if $1 eq 'SHA' or $1 eq 'SHA-1';
1451 sub get_cleartext_password {
1453 if($self->_password_encryption eq 'plain') {
1454 if($self->_password_encoding eq 'ldap') {
1455 $self->_password =~ /\{\w+\}(.*)$/;
1459 return $self->_password;
1468 Set the cleartext password for the account. If _password_encoding is set, the
1469 new password will be encoded according to the existing method (including
1470 encryption mode, if it can be determined). Otherwise,
1471 config('default-password-encoding') is used.
1473 If no password is supplied (or a zero-length password when minimum password length
1474 is >0), one will be generated randomly.
1479 my( $self, $pass ) = ( shift, shift );
1481 warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
1484 my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
1485 FS::Msgcat::_gettext('illegal_password_characters').
1488 my( $encoding, $encryption ) = ('', '');
1490 if ( $self->_password_encoding ) {
1491 $encoding = $self->_password_encoding;
1492 # identify existing encryption method, try to use it.
1493 $encryption = $self->_password_encryption;
1495 # use the system default
1501 # set encoding to system default
1502 ($encoding, $encryption) =
1503 split(/-/, lc($conf->config('default-password-encoding') || ''));
1504 $encoding ||= 'legacy';
1505 $self->_password_encoding($encoding);
1508 if ( $encoding eq 'legacy' ) {
1510 # The legacy behavior from check():
1511 # If the password is blank, randomize it and set encoding to 'plain'.
1512 if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
1513 $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
1514 $self->_password_encoding('plain');
1516 # Prefix + valid-length password
1517 if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1519 $self->_password_encoding('plain');
1520 # Prefix + crypt string
1521 } elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1523 $self->_password_encoding('crypt');
1524 # Various disabled crypt passwords
1525 } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
1526 $self->_password_encoding('crypt');
1532 $self->_password($pass);
1538 if $passwordmin && length($pass) < $passwordmin
1539 or $passwordmax && length($pass) > $passwordmax;
1541 if ( $encoding eq 'crypt' ) {
1542 if ($encryption eq 'md5') {
1543 $pass = unix_md5_crypt($pass);
1544 } elsif ($encryption eq 'des') {
1545 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1548 } elsif ( $encoding eq 'ldap' ) {
1549 if ($encryption eq 'md5') {
1550 $pass = md5_base64($pass);
1551 } elsif ($encryption eq 'sha1') {
1552 $pass = sha1_base64($pass);
1553 } elsif ($encryption eq 'crypt') {
1554 $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
1556 # else $encryption eq 'plain', do nothing
1557 $pass .= '=' x (4 - length($pass) % 4) #properly padded base64
1558 if $encryption eq 'md5' || $encryption eq 'sha1';
1559 $pass = '{'.uc($encryption).'}'.$pass;
1561 # else encoding eq 'plain'
1563 $self->_password($pass);
1569 Internal function to check the username against the list of system usernames
1570 from the I<system_usernames> configuration value. Returns true if the username
1571 is listed on the system username list.
1577 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1578 $conf->config('system_usernames')
1582 =item _check_duplicate
1584 Internal method to check for duplicates usernames, username@domain pairs and
1587 If the I<global_unique-username> configuration value is set to B<username> or
1588 B<username@domain>, enforces global username or username@domain uniqueness.
1590 In all cases, check for duplicate uids and usernames or username@domain pairs
1591 per export and with identical I<svcpart> values.
1595 sub _check_duplicate {
1598 my $global_unique = $conf->config('global_unique-username') || 'none';
1599 return '' if $global_unique eq 'disabled';
1603 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1604 unless ( $part_svc ) {
1605 return 'unknown svcpart '. $self->svcpart;
1608 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1609 qsearch( 'svc_acct', { 'username' => $self->username } );
1610 return gettext('username_in_use')
1611 if $global_unique eq 'username' && @dup_user;
1613 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1614 qsearch( 'svc_acct', { 'username' => $self->username,
1615 'domsvc' => $self->domsvc } );
1616 return gettext('username_in_use')
1617 if $global_unique eq 'username@domain' && @dup_userdomain;
1620 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1621 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1622 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1623 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1628 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1629 my $exports = FS::part_export::export_info('svc_acct');
1630 my %conflict_user_svcpart;
1631 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1633 foreach my $part_export ( $part_svc->part_export ) {
1635 #this will catch to the same exact export
1636 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1638 #this will catch to exports w/same exporthost+type ???
1639 #my @other_part_export = qsearch('part_export', {
1640 # 'machine' => $part_export->machine,
1641 # 'exporttype' => $part_export->exporttype,
1643 #foreach my $other_part_export ( @other_part_export ) {
1644 # push @svcparts, map { $_->svcpart }
1645 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1648 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1649 #silly kludge to avoid uninitialized value errors
1650 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1651 ? $exports->{$part_export->exporttype}{'nodomain'}
1653 if ( $nodomain =~ /^Y/i ) {
1654 $conflict_user_svcpart{$_} = $part_export->exportnum
1657 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1662 foreach my $dup_user ( @dup_user ) {
1663 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1664 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1665 return "duplicate username ". $self->username.
1666 ": conflicts with svcnum ". $dup_user->svcnum.
1667 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1671 foreach my $dup_userdomain ( @dup_userdomain ) {
1672 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1673 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1674 return "duplicate username\@domain ". $self->email.
1675 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1676 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1680 foreach my $dup_uid ( @dup_uid ) {
1681 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1682 if ( exists($conflict_user_svcpart{$dup_svcpart})
1683 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1684 return "duplicate uid ". $self->uid.
1685 ": conflicts with svcnum ". $dup_uid->svcnum.
1687 ( $conflict_user_svcpart{$dup_svcpart}
1688 || $conflict_userdomain_svcpart{$dup_svcpart} );
1700 Depriciated, use radius_reply instead.
1705 carp "FS::svc_acct::radius depriciated, use radius_reply";
1706 $_[0]->radius_reply;
1711 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1712 reply attributes of this record.
1714 Note that this is now the preferred method for reading RADIUS attributes -
1715 accessing the columns directly is discouraged, as the column names are
1716 expected to change in the future.
1723 return %{ $self->{'radius_reply'} }
1724 if exists $self->{'radius_reply'};
1729 my($column, $attrib) = ($1, $2);
1730 #$attrib =~ s/_/\-/g;
1731 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1732 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1734 if ( $self->slipip && $self->slipip ne '0e0' ) {
1735 $reply{$radius_ip} = $self->slipip;
1738 if ( $self->seconds !~ /^$/ ) {
1739 $reply{'Session-Timeout'} = $self->seconds;
1742 if ( $conf->exists('radius-chillispot-max') ) {
1743 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1745 #hmm. just because sqlradius.pm says so?
1752 foreach my $what (qw( input output total )) {
1753 my $is = $whatis{$what}.'bytes';
1754 if ( $self->$is() =~ /\d/ ) {
1755 my $big = new Math::BigInt $self->$is();
1756 $big = new Math::BigInt '0' if $big->is_neg();
1757 my $att = "Chillispot-Max-\u$what";
1758 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1759 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1770 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1771 check attributes of this record.
1773 Note that this is now the preferred method for reading RADIUS attributes -
1774 accessing the columns directly is discouraged, as the column names are
1775 expected to change in the future.
1782 return %{ $self->{'radius_check'} }
1783 if exists $self->{'radius_check'};
1788 my($column, $attrib) = ($1, $2);
1789 #$attrib =~ s/_/\-/g;
1790 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1791 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1794 my($pw_attrib, $password) = $self->radius_password;
1795 $check{$pw_attrib} = $password;
1797 my $cust_svc = $self->cust_svc;
1799 my $cust_pkg = $cust_svc->cust_pkg;
1800 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1801 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1804 warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
1805 "; can't set Expiration\n"
1813 =item radius_password
1815 Returns a key/value pair containing the RADIUS attribute name and value
1820 sub radius_password {
1824 if ( $self->_password_encoding eq 'ldap' ) {
1825 $pw_attrib = 'Password-With-Header';
1826 } elsif ( $self->_password_encoding eq 'crypt' ) {
1827 $pw_attrib = 'Crypt-Password';
1828 } elsif ( $self->_password_encoding eq 'plain' ) {
1829 $pw_attrib = $radius_password;
1831 $pw_attrib = length($self->_password) <= 12
1836 ($pw_attrib, $self->_password);
1842 This method instructs the object to "snapshot" or freeze RADIUS check and
1843 reply attributes to the current values.
1847 #bah, my english is too broken this morning
1848 #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
1849 #the FS::cust_pkg's replace method to trigger the correct export updates when
1850 #package dates change)
1855 $self->{$_} = { $self->$_() }
1856 foreach qw( radius_reply radius_check );
1860 =item forget_snapshot
1862 This methos instructs the object to forget any previously snapshotted
1863 RADIUS check and reply attributes.
1867 sub forget_snapshot {
1871 foreach qw( radius_reply radius_check );
1875 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1877 Returns the domain associated with this account.
1879 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1886 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1887 my $svc_domain = $self->svc_domain(@_)
1888 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1889 $svc_domain->domain;
1894 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1898 #inherited from svc_Common
1900 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1902 Returns an email address associated with the account.
1904 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1911 $self->username. '@'. $self->domain(@_);
1917 Returns an array of FS::acct_snarf records associated with the account.
1921 # unused as originally intended, but now by Communigate Pro "RPOP"
1925 'table' => 'acct_snarf',
1926 'hashref' => { 'svcnum' => $self->svcnum },
1927 #'order_by' => 'ORDER BY priority ASC',
1931 =item cgp_rpop_hashref
1933 Returns an arrayref of RPOP data suitable for Communigate Pro API commands.
1937 sub cgp_rpop_hashref {
1939 { map { $_->snarfname => $_->cgp_hashref } $self->acct_snarf };
1942 =item decrement_upbytes OCTETS
1944 Decrements the I<upbytes> field of this record by the given amount. If there
1945 is an error, returns the error, otherwise returns false.
1949 sub decrement_upbytes {
1950 shift->_op_usage('-', 'upbytes', @_);
1953 =item increment_upbytes OCTETS
1955 Increments the I<upbytes> field of this record by the given amount. If there
1956 is an error, returns the error, otherwise returns false.
1960 sub increment_upbytes {
1961 shift->_op_usage('+', 'upbytes', @_);
1964 =item decrement_downbytes OCTETS
1966 Decrements the I<downbytes> field of this record by the given amount. If there
1967 is an error, returns the error, otherwise returns false.
1971 sub decrement_downbytes {
1972 shift->_op_usage('-', 'downbytes', @_);
1975 =item increment_downbytes OCTETS
1977 Increments the I<downbytes> field of this record by the given amount. If there
1978 is an error, returns the error, otherwise returns false.
1982 sub increment_downbytes {
1983 shift->_op_usage('+', 'downbytes', @_);
1986 =item decrement_totalbytes OCTETS
1988 Decrements the I<totalbytes> field of this record by the given amount. If there
1989 is an error, returns the error, otherwise returns false.
1993 sub decrement_totalbytes {
1994 shift->_op_usage('-', 'totalbytes', @_);
1997 =item increment_totalbytes OCTETS
1999 Increments the I<totalbytes> field of this record by the given amount. If there
2000 is an error, returns the error, otherwise returns false.
2004 sub increment_totalbytes {
2005 shift->_op_usage('+', 'totalbytes', @_);
2008 =item decrement_seconds SECONDS
2010 Decrements the I<seconds> field of this record by the given amount. If there
2011 is an error, returns the error, otherwise returns false.
2015 sub decrement_seconds {
2016 shift->_op_usage('-', 'seconds', @_);
2019 =item increment_seconds SECONDS
2021 Increments the I<seconds> field of this record by the given amount. If there
2022 is an error, returns the error, otherwise returns false.
2026 sub increment_seconds {
2027 shift->_op_usage('+', 'seconds', @_);
2035 my %op2condition = (
2036 '-' => sub { my($self, $column, $amount) = @_;
2037 $self->$column - $amount <= 0;
2039 '+' => sub { my($self, $column, $amount) = @_;
2040 ($self->$column || 0) + $amount > 0;
2043 my %op2warncondition = (
2044 '-' => sub { my($self, $column, $amount) = @_;
2045 my $threshold = $column . '_threshold';
2046 $self->$column - $amount <= $self->$threshold + 0;
2048 '+' => sub { my($self, $column, $amount) = @_;
2049 ($self->$column || 0) + $amount > 0;
2054 my( $self, $op, $column, $amount ) = @_;
2056 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
2057 ' ('. $self->email. "): $op $amount\n"
2060 return '' unless $amount;
2062 local $SIG{HUP} = 'IGNORE';
2063 local $SIG{INT} = 'IGNORE';
2064 local $SIG{QUIT} = 'IGNORE';
2065 local $SIG{TERM} = 'IGNORE';
2066 local $SIG{TSTP} = 'IGNORE';
2067 local $SIG{PIPE} = 'IGNORE';
2069 my $oldAutoCommit = $FS::UID::AutoCommit;
2070 local $FS::UID::AutoCommit = 0;
2073 my $sql = "UPDATE svc_acct SET $column = ".
2074 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
2075 " $op ? WHERE svcnum = ?";
2079 my $sth = $dbh->prepare( $sql )
2080 or die "Error preparing $sql: ". $dbh->errstr;
2081 my $rv = $sth->execute($amount, $self->svcnum);
2082 die "Error executing $sql: ". $sth->errstr
2083 unless defined($rv);
2084 die "Can't update $column for svcnum". $self->svcnum
2087 #$self->snapshot; #not necessary, we retain the old values
2088 #create an object with the updated usage values
2089 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2091 my $error = $new->replace($self);
2093 $dbh->rollback if $oldAutoCommit;
2094 return "Error replacing: $error";
2097 #overlimit_action eq 'cancel' handling
2098 my $cust_pkg = $self->cust_svc->cust_pkg;
2100 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
2101 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
2105 my $error = $cust_pkg->cancel; #XXX should have a reason
2107 $dbh->rollback if $oldAutoCommit;
2108 return "Error cancelling: $error";
2111 #nothing else is relevant if we're cancelling, so commit & return success
2112 warn "$me update successful; committing\n"
2114 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2119 my $action = $op2action{$op};
2121 if ( &{$op2condition{$op}}($self, $column, $amount) &&
2122 ( $action eq 'suspend' && !$self->overlimit
2123 || $action eq 'unsuspend' && $self->overlimit )
2126 my $error = $self->_op_overlimit($action);
2128 $dbh->rollback if $oldAutoCommit;
2134 if ( $conf->exists("svc_acct-usage_$action")
2135 && &{$op2condition{$op}}($self, $column, $amount) ) {
2136 #my $error = $self->$action();
2137 my $error = $self->cust_svc->cust_pkg->$action();
2138 # $error ||= $self->overlimit($action);
2140 $dbh->rollback if $oldAutoCommit;
2141 return "Error ${action}ing: $error";
2145 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
2146 my $wqueue = new FS::queue {
2147 'svcnum' => $self->svcnum,
2148 'job' => 'FS::svc_acct::reached_threshold',
2153 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
2157 my $error = $wqueue->insert(
2158 'svcnum' => $self->svcnum,
2160 'column' => $column,
2164 $dbh->rollback if $oldAutoCommit;
2165 return "Error queuing threshold activity: $error";
2169 warn "$me update successful; committing\n"
2171 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2177 my( $self, $action ) = @_;
2179 local $SIG{HUP} = 'IGNORE';
2180 local $SIG{INT} = 'IGNORE';
2181 local $SIG{QUIT} = 'IGNORE';
2182 local $SIG{TERM} = 'IGNORE';
2183 local $SIG{TSTP} = 'IGNORE';
2184 local $SIG{PIPE} = 'IGNORE';
2186 my $oldAutoCommit = $FS::UID::AutoCommit;
2187 local $FS::UID::AutoCommit = 0;
2190 my $cust_pkg = $self->cust_svc->cust_pkg;
2192 my @conf_overlimit =
2194 ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
2195 : $conf->config('overlimit_groups');
2197 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
2199 my @groups = scalar(@conf_overlimit) ? @conf_overlimit
2200 : split(' ',$part_export->option('overlimit_groups'));
2201 next unless scalar(@groups);
2203 my $other = new FS::svc_acct $self->hashref;
2204 $other->usergroup(\@groups);
2207 if ($action eq 'suspend') {
2210 } else { # $action eq 'unsuspend'
2215 my $error = $part_export->export_replace($new, $old)
2216 || $self->overlimit($action);
2219 $dbh->rollback if $oldAutoCommit;
2220 return "Error replacing radius groups: $error";
2225 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2231 my( $self, $valueref, %options ) = @_;
2233 warn "$me set_usage called for svcnum ". $self->svcnum.
2234 ' ('. $self->email. "): ".
2235 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
2238 local $SIG{HUP} = 'IGNORE';
2239 local $SIG{INT} = 'IGNORE';
2240 local $SIG{QUIT} = 'IGNORE';
2241 local $SIG{TERM} = 'IGNORE';
2242 local $SIG{TSTP} = 'IGNORE';
2243 local $SIG{PIPE} = 'IGNORE';
2245 local $FS::svc_Common::noexport_hack = 1;
2246 my $oldAutoCommit = $FS::UID::AutoCommit;
2247 local $FS::UID::AutoCommit = 0;
2252 if ( $options{null} ) {
2253 %handyhash = ( map { ( $_ => undef, $_."_threshold" => undef ) }
2254 qw( seconds upbytes downbytes totalbytes )
2257 foreach my $field (keys %$valueref){
2258 $reset = 1 if $valueref->{$field};
2259 $self->setfield($field, $valueref->{$field});
2260 $self->setfield( $field.'_threshold',
2261 int($self->getfield($field)
2262 * ( $conf->exists('svc_acct-usage_threshold')
2263 ? 1 - $conf->config('svc_acct-usage_threshold')/100
2268 $handyhash{$field} = $self->getfield($field);
2269 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
2271 #my $error = $self->replace; #NO! we avoid the call to ->check for
2272 #die $error if $error; #services not explicity changed via the UI
2274 my $sql = "UPDATE svc_acct SET " .
2275 join (',', map { "$_ = ?" } (keys %handyhash) ).
2276 " WHERE svcnum = ". $self->svcnum;
2281 if (scalar(keys %handyhash)) {
2282 my $sth = $dbh->prepare( $sql )
2283 or die "Error preparing $sql: ". $dbh->errstr;
2284 my $rv = $sth->execute(values %handyhash);
2285 die "Error executing $sql: ". $sth->errstr
2286 unless defined($rv);
2287 die "Can't update usage for svcnum ". $self->svcnum
2291 #$self->snapshot; #not necessary, we retain the old values
2292 #create an object with the updated usage values
2293 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
2294 local($FS::Record::nowarn_identical) = 1;
2295 my $error = $new->replace($self); #call exports
2297 $dbh->rollback if $oldAutoCommit;
2298 return "Error replacing: $error";
2305 $error = $self->_op_overlimit('unsuspend')
2306 if $self->overlimit;;
2308 $error ||= $self->cust_svc->cust_pkg->unsuspend
2309 if $conf->exists("svc_acct-usage_unsuspend");
2312 $dbh->rollback if $oldAutoCommit;
2313 return "Error unsuspending: $error";
2318 warn "$me update successful; committing\n"
2320 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2326 =item recharge HASHREF
2328 Increments usage columns by the amount specified in HASHREF as
2329 column=>amount pairs.
2334 my ($self, $vhash) = @_;
2337 warn "[$me] recharge called on $self: ". Dumper($self).
2338 "\nwith vhash: ". Dumper($vhash);
2341 my $oldAutoCommit = $FS::UID::AutoCommit;
2342 local $FS::UID::AutoCommit = 0;
2346 foreach my $column (keys %$vhash){
2347 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
2351 $dbh->rollback if $oldAutoCommit;
2353 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2358 =item is_rechargeable
2360 Returns true if this svc_account can be "recharged" and false otherwise.
2364 sub is_rechargable {
2366 $self->seconds ne ''
2367 || $self->upbytes ne ''
2368 || $self->downbytes ne ''
2369 || $self->totalbytes ne '';
2372 =item seconds_since TIMESTAMP
2374 Returns the number of seconds this account has been online since TIMESTAMP,
2375 according to the session monitor (see L<FS::Session>).
2377 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2378 L<Time::Local> and L<Date::Parse> for conversion functions.
2382 #note: POD here, implementation in FS::cust_svc
2385 $self->cust_svc->seconds_since(@_);
2388 =item last_login_text
2390 Returns text describing the time of last login.
2394 sub last_login_text {
2396 $self->last_login ? ctime($self->last_login) : 'unknown';
2399 =item psearch_cdrs OPTIONS
2401 Returns a paged search (L<FS::PagedSearch>) for Call Detail Records
2402 associated with this service. For svc_acct, "associated with" means that
2403 either the "src" or the "charged_party" field of the CDR matches the
2404 "username" field of the service.
2409 my($self, %options) = @_;
2414 my $did = dbh->quote($self->username);
2416 my $prefix = $options{'default_prefix'} || ''; #convergent.au '+61'
2417 my $prefixdid = dbh->quote($prefix . $self->username);
2419 my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
2421 if ( $options{inbound} ) {
2422 # these will be selected under their DIDs
2423 push @where, "FALSE";
2427 if (!$options{'disable_charged_party'}) {
2429 "charged_party = $did",
2430 "charged_party = $prefixdid";
2432 if (!$options{'disable_src'}) {
2434 "src = $did AND charged_party IS NULL",
2435 "src = $prefixdid AND charged_party IS NULL";
2437 push @where, '(' . join(' OR ', @orwhere) . ')';
2439 # $options{'status'} = '' is meaningful; for the rest of them it's not
2440 if ( exists $options{'status'} ) {
2441 $hash{'freesidestatus'} = $options{'status'};
2443 if ( $options{'cdrtypenum'} ) {
2444 $hash{'cdrtypenum'} = $options{'cdrtypenum'};
2446 if ( $options{'calltypenum'} ) {
2447 $hash{'calltypenum'} = $options{'calltypenum'};
2449 if ( $options{'begin'} ) {
2450 push @where, 'startdate >= '. $options{'begin'};
2452 if ( $options{'end'} ) {
2453 push @where, 'startdate < '. $options{'end'};
2455 if ( $options{'nonzero'} ) {
2456 push @where, 'duration > 0';
2459 my $extra_sql = join(' AND ', @where);
2462 $extra_sql = " AND ".$extra_sql;
2464 $extra_sql = " WHERE ".$extra_sql;
2470 'hashref' => \%hash,
2471 'extra_sql' => $extra_sql,
2472 'order_by' => "ORDER BY startdate $for_update",
2476 =item get_cdrs (DEPRECATED)
2478 Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a
2479 single list. Arguments are the same as for psearch_cdrs.
2485 my $psearch = $self->psearch_cdrs(@_);
2486 qsearch ( $psearch->{query} )
2489 # sub radius_groups has moved to svc_Radius_Mixin
2491 =item clone_suspended
2493 Constructor used by FS::part_export::_export_suspend fallback. Document
2498 sub clone_suspended {
2500 my %hash = $self->hash;
2501 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2502 new FS::svc_acct \%hash;
2505 =item clone_kludge_unsuspend
2507 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2512 sub clone_kludge_unsuspend {
2514 my %hash = $self->hash;
2515 $hash{_password} = '';
2516 new FS::svc_acct \%hash;
2519 =item check_password
2521 Checks the supplied password against the (possibly encrypted) password in the
2522 database. Returns true for a successful authentication, false for no match.
2524 Currently supported encryptions are: classic DES crypt() and MD5
2528 sub check_password {
2529 my($self, $check_password) = @_;
2531 #remove old-style SUSPENDED kludge, they should be allowed to login to
2532 #self-service and pay up
2533 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2535 if ( $self->_password_encoding eq 'ldap' ) {
2537 $password =~ s/^{PLAIN}/{CLEARTEXT}/;
2538 my $auth = from_rfc2307 Authen::Passphrase $password;
2539 return $auth->match($check_password);
2541 } elsif ( $self->_password_encoding eq 'crypt' ) {
2543 my $auth = from_crypt Authen::Passphrase $self->_password;
2544 return $auth->match($check_password);
2546 } elsif ( $self->_password_encoding eq 'plain' ) {
2548 return $check_password eq $password;
2552 #XXX this could be replaced with Authen::Passphrase stuff
2554 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2556 } elsif ( length($password) < 13 ) { #plaintext
2557 $check_password eq $password;
2558 } elsif ( length($password) == 13 ) { #traditional DES crypt
2559 crypt($check_password, $password) eq $password;
2560 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2561 unix_md5_crypt($check_password, $password) eq $password;
2562 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2563 warn "Can't check password: Blowfish encryption not yet supported, ".
2564 "svcnum ". $self->svcnum. "\n";
2567 warn "Can't check password: Unrecognized encryption for svcnum ".
2568 $self->svcnum. "\n";
2576 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2578 Returns an encrypted password, either by passing through an encrypted password
2579 in the database or by encrypting a plaintext password from the database.
2581 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2582 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2583 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2584 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2585 encryption type is only used if the password is not already encrypted in the
2590 sub crypt_password {
2593 if ( $self->_password_encoding eq 'ldap' ) {
2595 if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
2598 #XXX this could be replaced with Authen::Passphrase stuff
2600 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2601 if ( $encryption eq 'crypt' ) {
2604 $saltset[int(rand(64))].$saltset[int(rand(64))]
2606 } elsif ( $encryption eq 'md5' ) {
2607 return unix_md5_crypt( $self->_password );
2608 } elsif ( $encryption eq 'blowfish' ) {
2609 croak "unknown encryption method $encryption";
2611 croak "unknown encryption method $encryption";
2614 } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
2618 } elsif ( $self->_password_encoding eq 'crypt' ) {
2620 return $self->_password;
2622 } elsif ( $self->_password_encoding eq 'plain' ) {
2624 #XXX this could be replaced with Authen::Passphrase stuff
2626 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2627 if ( $encryption eq 'crypt' ) {
2630 $saltset[int(rand(64))].$saltset[int(rand(64))]
2632 } elsif ( $encryption eq 'md5' ) {
2633 return unix_md5_crypt( $self->_password );
2634 } elsif ( $encryption eq 'sha1_base64' ) { #for acct_sql
2635 my $pass = sha1_base64( $self->_password );
2636 $pass .= '=' x (4 - length($pass) % 4); #properly padded base64
2638 } elsif ( $encryption eq 'blowfish' ) {
2639 croak "unknown encryption method $encryption";
2641 croak "unknown encryption method $encryption";
2646 if ( length($self->_password) == 13
2647 || $self->_password =~ /^\$(1|2a?)\$/
2648 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2654 #XXX this could be replaced with Authen::Passphrase stuff
2656 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2657 if ( $encryption eq 'crypt' ) {
2660 $saltset[int(rand(64))].$saltset[int(rand(64))]
2662 } elsif ( $encryption eq 'md5' ) {
2663 return unix_md5_crypt( $self->_password );
2664 } elsif ( $encryption eq 'blowfish' ) {
2665 croak "unknown encryption method $encryption";
2667 croak "unknown encryption method $encryption";
2676 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2678 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2679 describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
2680 "{MD5}5426824942db4253f87a1009fd5d2d4".
2682 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2683 to work the same as the B</crypt_password> method.
2689 #eventually should check a "password-encoding" field
2691 if ( $self->_password_encoding eq 'ldap' ) {
2693 return $self->_password;
2695 } elsif ( $self->_password_encoding eq 'crypt' ) {
2697 if ( length($self->_password) == 13 ) { #crypt
2698 return '{CRYPT}'. $self->_password;
2699 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2701 #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2702 # die "Blowfish encryption not supported in this context, svcnum ".
2703 # $self->svcnum. "\n";
2705 warn "encryption method not (yet?) supported in LDAP context";
2706 return '{CRYPT}*'; #unsupported, should not auth
2709 } elsif ( $self->_password_encoding eq 'plain' ) {
2711 return '{PLAIN}'. $self->_password;
2713 #return '{CLEARTEXT}'. $self->_password; #?
2717 if ( length($self->_password) == 13 ) { #crypt
2718 return '{CRYPT}'. $self->_password;
2719 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2721 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2722 warn "Blowfish encryption not supported in this context, svcnum ".
2723 $self->svcnum. "\n";
2726 #are these two necessary anymore?
2727 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2728 return '{SSHA}'. $1;
2729 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2730 return '{NS-MTA-MD5}'. $1;
2733 return '{PLAIN}'. $self->_password;
2735 #return '{CLEARTEXT}'. $self->_password; #?
2737 #XXX this could be replaced with Authen::Passphrase stuff if it gets used
2738 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2739 #if ( $encryption eq 'crypt' ) {
2740 # return '{CRYPT}'. crypt(
2742 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2744 #} elsif ( $encryption eq 'md5' ) {
2745 # unix_md5_crypt( $self->_password );
2746 #} elsif ( $encryption eq 'blowfish' ) {
2747 # croak "unknown encryption method $encryption";
2749 # croak "unknown encryption method $encryption";
2757 =item domain_slash_username
2759 Returns $domain/$username/
2763 sub domain_slash_username {
2765 $self->domain. '/'. $self->username. '/';
2768 =item virtual_maildir
2770 Returns $domain/maildirs/$username/
2774 sub virtual_maildir {
2776 $self->domain. '/maildirs/'. $self->username. '/';
2779 =item password_svc_check
2781 Override, for L<FS::Password_Mixin>. Not really intended for other use.
2785 sub password_svc_check {
2786 my ($self, $password) = @_;
2787 foreach my $field ( qw(username finger) ) {
2788 foreach my $word (split(/\W+/,$self->get($field))) {
2789 next unless length($word) > 2;
2790 if ($password =~ /$word/i) {
2791 return qq(Password contains account information '$word');
2800 =head1 CLASS METHODS
2804 =item search HASHREF
2806 Class method which returns a qsearch hash expression to search for parameters
2807 specified in HASHREF. Valid parameters are
2821 Arrayref of pkgparts
2827 Arrayref of additional WHERE clauses, will be ANDed together.
2838 my( $class, $params, $from, $where ) = @_;
2840 #these two should probably move to svc_Domain_Mixin ?
2843 if ( $params->{'domain'} ) {
2844 my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
2845 #preserve previous behavior & bubble up an error if $svc_domain not found?
2846 push @$where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
2850 if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
2851 push @$where, "domsvc = $1";
2856 if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
2857 push @$where, "popnum = $1";
2861 #and these in svc_Tower_Mixin, or maybe we never should have done svc_acct
2862 # towers (or, as mark thought, never should have done svc_broadband)
2865 my @where_sector = $class->tower_sector_sql($params);
2866 if ( @where_sector ) {
2867 push @$where, @where_sector;
2868 push @$from, ' LEFT JOIN tower_sector USING ( sectornum )';
2881 This is the FS::svc_acct job-queue-able version. It still uses
2882 FS::Misc::send_email under-the-hood.
2889 eval "use FS::Misc qw(send_email)";
2892 $opt{mimetype} ||= 'text/plain';
2893 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2895 my $error = send_email(
2896 'from' => $opt{from},
2898 'subject' => $opt{subject},
2899 'content-type' => $opt{mimetype},
2900 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2902 die $error if $error;
2905 =item check_and_rebuild_fuzzyfiles
2909 sub check_and_rebuild_fuzzyfiles {
2910 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2911 -e "$dir/svc_acct.username"
2912 or &rebuild_fuzzyfiles;
2915 =item rebuild_fuzzyfiles
2919 sub rebuild_fuzzyfiles {
2921 use Fcntl qw(:flock);
2923 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2927 open(USERNAMELOCK,">>$dir/svc_acct.username")
2928 or die "can't open $dir/svc_acct.username: $!";
2929 flock(USERNAMELOCK,LOCK_EX)
2930 or die "can't lock $dir/svc_acct.username: $!";
2932 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2934 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2935 or die "can't open $dir/svc_acct.username.tmp: $!";
2936 print USERNAMECACHE join("\n", @all_username), "\n";
2937 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2939 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2949 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2950 open(USERNAMECACHE,"<$dir/svc_acct.username")
2951 or die "can't open $dir/svc_acct.username: $!";
2952 my @array = map { chomp; $_; } <USERNAMECACHE>;
2953 close USERNAMECACHE;
2957 =item append_fuzzyfiles USERNAME
2961 sub append_fuzzyfiles {
2962 my $username = shift;
2964 &check_and_rebuild_fuzzyfiles;
2966 use Fcntl qw(:flock);
2968 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2970 open(USERNAME,">>$dir/svc_acct.username")
2971 or die "can't open $dir/svc_acct.username: $!";
2972 flock(USERNAME,LOCK_EX)
2973 or die "can't lock $dir/svc_acct.username: $!";
2975 print USERNAME "$username\n";
2977 flock(USERNAME,LOCK_UN)
2978 or die "can't unlock $dir/svc_acct.username: $!";
2985 =item reached_threshold
2987 Performs some activities when svc_acct thresholds (such as number of seconds
2988 remaining) are reached.
2992 sub reached_threshold {
2995 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2996 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2998 if ( $opt{'op'} eq '+' ){
2999 $svc_acct->setfield( $opt{'column'}.'_threshold',
3000 int($svc_acct->getfield($opt{'column'})
3001 * ( $conf->exists('svc_acct-usage_threshold')
3002 ? $conf->config('svc_acct-usage_threshold')/100
3007 my $error = $svc_acct->replace;
3008 die $error if $error;
3009 }elsif ( $opt{'op'} eq '-' ){
3011 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
3012 return '' if ($threshold eq '' );
3014 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
3015 my $error = $svc_acct->replace;
3016 die $error if $error; # email next time, i guess
3018 if ( $warning_template ) {
3019 eval "use FS::Misc qw(send_email)";
3022 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
3023 my $cust_main = $cust_pkg->cust_main;
3025 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
3026 $cust_main->invoicing_list,
3027 ($opt{'to'} ? $opt{'to'} : ())
3030 my $mimetype = $warning_mimetype;
3031 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
3033 my $body = $warning_template->fill_in( HASH => {
3034 'custnum' => $cust_main->custnum,
3035 'username' => $svc_acct->username,
3036 'password' => $svc_acct->_password,
3037 'first' => $cust_main->first,
3038 'last' => $cust_main->getfield('last'),
3039 'pkg' => $cust_pkg->part_pkg->pkg,
3040 'column' => $opt{'column'},
3041 'amount' => $opt{'column'} =~/bytes/
3042 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
3043 : $svc_acct->getfield($opt{'column'}),
3044 'threshold' => $opt{'column'} =~/bytes/
3045 ? FS::UI::bytecount::display_bytecount($threshold)
3050 my $error = send_email(
3051 'from' => $warning_from,
3053 'subject' => $warning_subject,
3054 'content-type' => $mimetype,
3055 'body' => [ map "$_\n", split("\n", $body) ],
3057 die $error if $error;
3060 die "unknown op: " . $opt{'op'};
3068 The $recref stuff in sub check should be cleaned up.
3070 The suspend, unsuspend and cancel methods update the database, but not the
3071 current object. This is probably a bug as it's unexpected and
3074 insertion of RADIUS group stuff in insert could be done with child_objects now
3075 (would probably clean up export of them too)
3077 _op_usage and set_usage bypass the history... maybe they shouldn't
3081 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
3082 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
3083 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
3084 L<freeside-queued>), L<FS::svc_acct_pop>,
3085 schema.html from the base documentation.