X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fsvc_acct.pm;h=480290309ba066cd07d9269cad5d801afffc5b00;hb=12f52398d69509cd8823d1ef420e3c4d032688d3;hp=8c1c350fa08d2dd1cdf0dcc799b20c1ea7173e82;hpb=bd368448838fb00212fa34d70e467cf4c8e12206;p=freeside.git diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 8c1c350fa..480290309 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -8,7 +8,8 @@ use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles $username_noperiod $username_nounderscore $username_nodash $username_uppercase $username_percent $password_noampersand $password_noexclamation - $welcome_template $welcome_from $welcome_subject $welcome_mimetype + $welcome_template $welcome_from + $welcome_subject $welcome_subject_template $welcome_mimetype $warning_template $warning_from $warning_subject $warning_mimetype $warning_cc $smtpmachine @@ -20,10 +21,11 @@ use Fcntl qw(:flock); use Date::Format; use Crypt::PasswdMD5 1.2; use Data::Dumper; -use FS::UID qw( datasrc ); +use FS::UID qw( datasrc driver_name ); use FS::Conf; use FS::Record qw( qsearch qsearchs fields dbh dbdef ); use FS::Msgcat qw(gettext); +use FS::UI::bytecount; use FS::svc_Common; use FS::cust_svc; use FS::part_svc; @@ -71,6 +73,10 @@ $FS::UID::callback{'FS::svc_acct'} = sub { ) or warn "can't create welcome email template: $Text::Template::ERROR"; $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum' $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome'; + $welcome_subject_template = new Text::Template ( + TYPE => 'STRING', + SOURCE => $welcome_subject, + ) or warn "can't create welcome email subject template: $Text::Template::ERROR"; $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain'; } else { $welcome_template = ''; @@ -97,6 +103,7 @@ $FS::UID::callback{'FS::svc_acct'} = sub { $smtpmachine = $conf->config('smtpmachine'); $radius_password = $conf->config('radius-password') || 'Password'; $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address'; + @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps'); }; @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' ); @@ -210,6 +217,139 @@ Creates a new account. To add the account to the database, see L<"insert">. =cut +sub table_info { + { + 'name' => 'Account', + 'longname_plural' => 'Access accounts and mailboxes', + 'sorts' => [ 'username', 'uid', 'last_login', ], + 'display_weight' => 10, + 'cancel_weight' => 50, + 'fields' => { + 'dir' => 'Home directory', + 'uid' => { + label => 'UID', + def_label => 'UID (set to fixed and blank for no UIDs)', + type => 'text', + }, + 'slipip' => 'IP address', + # 'popnum' => qq!POP number!, + 'popnum' => { + label => 'Access number', + type => 'select', + select_table => 'svc_acct_pop', + select_key => 'popnum', + select_label => 'city', + disable_select => 1, + }, + 'username' => { + label => 'Username', + type => 'text', + disable_default => 1, + disable_fixed => 1, + disable_select => 1, + }, + 'quota' => { + label => 'Quota', + type => 'text', + disable_inventory => 1, + disable_select => 1, + }, + '_password' => 'Password', + 'gid' => { + label => 'GID', + def_label => 'GID (when blank, defaults to UID)', + type => 'text', + }, + 'shell' => { + #desc =>'Shell (all service definitions should have a default or fixed shell that is present in the shells configuration file, set to blank for no shell tracking)', + label => 'Shell', + def_label=> 'Shell (set to blank for no shell tracking)', + type =>'select', + select_list => [ $conf->config('shells') ], + disable_inventory => 1, + disable_select => 1, + }, + 'finger' => 'Real name (GECOS)', + 'domsvc' => { + label => 'Domain', + #def_label => 'svcnum from svc_domain', + type => 'select', + select_table => 'svc_domain', + select_key => 'svcnum', + select_label => 'domain', + disable_inventory => 1, + + }, + 'usergroup' => { + label => 'RADIUS groups', + type => 'radius_usergroup_selector', + disable_inventory => 1, + disable_select => 1, + }, + 'seconds' => { label => 'Seconds', + type => 'text', + disable_inventory => 1, + disable_select => 1, + }, + 'upbytes' => { label => 'Upload', + type => 'text', + disable_inventory => 1, + disable_select => 1, + 'format' => \&FS::UI::bytecount::display_bytecount, + 'parse' => \&FS::UI::bytecount::parse_bytecount, + }, + 'downbytes' => { label => 'Download', + type => 'text', + disable_inventory => 1, + disable_select => 1, + 'format' => \&FS::UI::bytecount::display_bytecount, + 'parse' => \&FS::UI::bytecount::parse_bytecount, + }, + 'totalbytes'=> { label => 'Total up and download', + type => 'text', + disable_inventory => 1, + disable_select => 1, + 'format' => \&FS::UI::bytecount::display_bytecount, + 'parse' => \&FS::UI::bytecount::parse_bytecount, + }, + 'seconds_threshold' => { label => 'Seconds threshold', + type => 'text', + disable_inventory => 1, + disable_select => 1, + }, + 'upbytes_threshold' => { label => 'Upload threshold', + type => 'text', + disable_inventory => 1, + disable_select => 1, + 'format' => \&FS::UI::bytecount::display_bytecount, + 'parse' => \&FS::UI::bytecount::parse_bytecount, + }, + 'downbytes_threshold' => { label => 'Download threshold', + type => 'text', + disable_inventory => 1, + disable_select => 1, + 'format' => \&FS::UI::bytecount::display_bytecount, + 'parse' => \&FS::UI::bytecount::parse_bytecount, + }, + 'totalbytes_threshold'=> { label => 'Total up and download threshold', + type => 'text', + disable_inventory => 1, + disable_select => 1, + 'format' => \&FS::UI::bytecount::display_bytecount, + 'parse' => \&FS::UI::bytecount::parse_bytecount, + }, + 'last_login'=> { + label => 'Last login', + type => 'disabled', + }, + 'last_logout'=> { + label => 'Last logout', + type => 'disabled', + }, + }, + }; +} + sub table { 'svc_acct'; } sub _fieldhandlers { @@ -228,6 +368,88 @@ sub _fieldhandlers { }; } +sub last_login { + shift->_lastlog('in', @_); +} + +sub last_logout { + shift->_lastlog('out', @_); +} + +sub _lastlog { + my( $self, $op, $time ) = @_; + + if ( defined($time) ) { + warn "$me last_log$op called on svcnum ". $self->svcnum. + ' ('. $self->email. "): $time\n" + if $DEBUG; + + my $dbh = dbh; + + my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?"; + warn "$me $sql\n" + if $DEBUG; + + my $sth = $dbh->prepare( $sql ) + or die "Error preparing $sql: ". $dbh->errstr; + my $rv = $sth->execute($time, $self->svcnum); + die "Error executing $sql: ". $sth->errstr + unless defined($rv); + die "Can't update last_log$op for svcnum". $self->svcnum + if $rv == 0; + + $self->{'Hash'}->{"last_log$op"} = $time; + }else{ + $self->getfield("last_log$op"); + } +} + +=item search_sql STRING + +Class method which returns an SQL fragment to search for the given string. + +=cut + +sub search_sql { + my( $class, $string ) = @_; + if ( $string =~ /^([^@]+)@([^@]+)$/ ) { + my( $username, $domain ) = ( $1, $2 ); + my $q_username = dbh->quote($username); + my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } ); + if ( @svc_domain ) { + "svc_acct.username = $q_username AND ( ". + join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ). + " )"; + } else { + '1 = 0'; #false + } + } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) { + ' ( '. + $class->search_sql_field('slipip', $string ). + ' OR '. + $class->search_sql_field('username', $string ). + ' ) '; + } else { + $class->search_sql_field('username', $string); + } +} + +=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ] + +Returns the "username@domain" string for this account. + +END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with +history records. + +=cut + +sub label { + my $self = shift; + $self->email(@_); +} + +=cut + =item insert [ , OPTION => VALUE ... ] Adds this account to the database. If there is an error, returns the error, @@ -336,7 +558,10 @@ sub insert { if ( $cust_pkg ) { my $cust_main = $cust_pkg->cust_main; - if ( $conf->exists('emailinvoiceauto') ) { + if ( $conf->exists('emailinvoiceautoalways') + || $conf->exists('emailinvoiceauto') + && ! $cust_main->invoicing_list_emailonly + ) { my @invoicing_list = $cust_main->invoicing_list; push @invoicing_list, $self->email; $cust_main->invoicing_list(\@invoicing_list); @@ -347,6 +572,15 @@ sub insert { if ( $welcome_template && $cust_pkg ) { my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ); if ( $to ) { + + my %hash = ( + 'custnum' => $self->custnum, + 'username' => $self->username, + 'password' => $self->_password, + 'first' => $cust_main->first, + 'last' => $cust_main->getfield('last'), + 'pkg' => $cust_pkg->part_pkg->pkg, + ); my $wqueue = new FS::queue { 'svcnum' => $self->svcnum, 'job' => 'FS::svc_acct::send_email' @@ -354,16 +588,9 @@ sub insert { my $error = $wqueue->insert( 'to' => $to, 'from' => $welcome_from, - 'subject' => $welcome_subject, + 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ), 'mimetype' => $welcome_mimetype, - 'body' => $welcome_template->fill_in( HASH => { - 'custnum' => $self->custnum, - 'username' => $self->username, - 'password' => $self->_password, - 'first' => $cust_main->first, - 'last' => $cust_main->getfield('last'), - 'pkg' => $cust_pkg->part_pkg->pkg, - } ), + 'body' => $welcome_template->fill_in( HASH => \%hash, ), ); if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -472,6 +699,12 @@ sub delete { } } + my $error = $self->SUPER::delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + foreach my $radius_usergroup ( qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } ) ) { @@ -482,12 +715,6 @@ sub delete { } } - my $error = $self->SUPER::delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -959,11 +1186,19 @@ sub _check_duplicate { my $global_unique = $conf->config('global_unique-username') || 'none'; return '' if $global_unique eq 'disabled'; - #this is Pg-specific. what to do for mysql etc? - # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ ) warn "$me locking svc_acct table for duplicate search" if $DEBUG; - dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE") - or die dbh->errstr; + if ( driver_name =~ /^Pg/i ) { + dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE") + or die dbh->errstr; + } elsif ( driver_name =~ /^mysql/i ) { + dbh->do("SELECT * FROM duplicate_lock + WHERE lockname = 'svc_acct' + FOR UPDATE" + ) or die dbh->errstr; + } else { + die "unknown database ". driver_name. + "; don't know how to lock for duplicate search"; + } warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG; my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } ); @@ -1180,10 +1415,13 @@ sub forget_snapshot { } -=item domain +=item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ] Returns the domain associated with this account. +END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with +history records. + =cut sub domain { @@ -1201,6 +1439,8 @@ L). =cut +# FS::h_svc_acct has a history-aware svc_domain override + sub svc_domain { my $self = shift; $self->{'_domsvc'} @@ -1216,10 +1456,13 @@ Returns the FS::cust_svc record for this account (see L). #inherited from svc_Common -=item email +=item email [ END_TIMESTAMP [ START_TIMESTAMP ] ] Returns an email address associated with the account. +END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with +history records. + =cut sub email { @@ -1390,10 +1633,37 @@ sub _op_usage { my $action = $op2action{$op}; + if ( &{$op2condition{$op}}($self, $column, $amount) && + ( $action eq 'suspend' && !$self->overlimit + || $action eq 'unsuspend' && $self->overlimit ) + ) { + foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { + if ($part_export->option('overlimit_groups')) { + my ($new,$old); + my $other = new FS::svc_acct $self->hashref; + my $groups = &{ $self->_fieldhandlers->{'usergroup'} } + ($self, $part_export->option('overlimit_groups')); + $other->usergroup( $groups ); + if ($action eq 'suspend'){ + $new = $other; $old = $self; + }else{ + $new = $self; $old = $other; + } + my $error = $part_export->export_replace($new, $old); + $error ||= $self->overlimit($action); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error replacing radius groups in export, ${op}: $error"; + } + } + } + } + if ( $conf->exists("svc_acct-usage_$action") && &{$op2condition{$op}}($self, $column, $amount) ) { #my $error = $self->$action(); my $error = $self->cust_svc->cust_pkg->$action(); + # $error ||= $self->overlimit($action); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "Error ${action}ing: $error"; @@ -1446,11 +1716,13 @@ sub set_usage { local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; + local $FS::svc_Common::noexport_hack = 1; my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; my $reset = 0; + my %handyhash = (); foreach my $field (keys %$valueref){ $reset = 1 if $valueref->{$field}; $self->setfield($field, $valueref->{$field}); @@ -1462,12 +1734,48 @@ sub set_usage { ) ) ); + $handyhash{$field} = $self->getfield($field); + $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold'); } - my $error = $self->replace; - die $error if $error; + #my $error = $self->replace; #NO! we avoid the call to ->check for + #die $error if $error; #services not explicity changed via the UI + + my $sql = "UPDATE svc_acct SET " . + join (',', map { "$_ = ?" } (keys %handyhash) ). + " WHERE svcnum = ?"; + + warn "$me $sql\n" + if $DEBUG; + + if (scalar(keys %handyhash)) { + my $sth = $dbh->prepare( $sql ) + or die "Error preparing $sql: ". $dbh->errstr; + my $rv = $sth->execute((values %handyhash), $self->svcnum); + die "Error executing $sql: ". $sth->errstr + unless defined($rv); + die "Can't update usage for svcnum ". $self->svcnum + if $rv == 0; + } + + if ( $reset ) { + my $error; + + if ($self->overlimit) { + $error = $self->overlimit('unsuspend'); + foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { + if ($part_export->option('overlimit_groups')) { + my $old = new FS::svc_acct $self->hashref; + my $groups = &{ $self->_fieldhandlers->{'usergroup'} } + ($self, $part_export->option('overlimit_groups')); + $old->usergroup( $groups ); + $error ||= $part_export->export_replace($self, $old); + } + } + } - if ( $conf->exists("svc_acct-usage_unsuspend") && $reset ) { - my $error = $self->cust_svc->cust_pkg->unsuspend; + if ( $conf->exists("svc_acct-usage_unsuspend")) { + $error ||= $self->cust_svc->cust_pkg->unsuspend; + } if ( $error ) { $dbh->rollback if $oldAutoCommit; return "Error unsuspending: $error"; @@ -1598,6 +1906,17 @@ sub get_session_history { $self->cust_svc->get_session_history(@_); } +=item last_login_text + +Returns text describing the time of last login. + +=cut + +sub last_login_text { + my $self = shift; + $self->last_login ? ctime($self->last_login) : 'unknown'; +} + =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ] =cut @@ -1803,8 +2122,9 @@ sub ldap_password { } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5 return '{MD5}'. $1; } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish - die "Blowfish encryption not supported in this context, svcnum ". - $self->svcnum. "\n"; + warn "Blowfish encryption not supported in this context, svcnum ". + $self->svcnum. "\n"; + return '{CRYPT}*'; #unsupported, should not auth } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA return '{SSHA}'. $1; } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5 @@ -2053,7 +2373,6 @@ sub reached_threshold { my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list, - $svc_acct->email, ($opt{'to'} ? $opt{'to'} : ()) ); @@ -2068,8 +2387,12 @@ sub reached_threshold { 'last' => $cust_main->getfield('last'), 'pkg' => $cust_pkg->part_pkg->pkg, 'column' => $opt{'column'}, - 'amount' => $svc_acct->getfield($opt{'column'}), - 'threshold' => $threshold, + 'amount' => $opt{'column'} =~/bytes/ + ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'})) + : $svc_acct->getfield($opt{'column'}), + 'threshold' => $opt{'column'} =~/bytes/ + ? FS::UI::bytecount::display_bytecount($threshold) + : $threshold, } ); @@ -2113,5 +2436,61 @@ schema.html from the base documentation. =cut +=item domain_select_hash %OPTIONS + +Returns a hash SVCNUM => DOMAIN ... representing the domains this customer +may at present purchase. + +Currently available options are: I I + +=cut + +sub domain_select_hash { + my ($self, %options) = @_; + my %domains = (); + my $part_svc; + my $cust_pkg; + + if (ref($self)) { + $part_svc = $self->part_svc; + $cust_pkg = $self->cust_svc->cust_pkg + if $self->cust_svc; + } + + $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} }) + if $options{'svcpart'}; + + $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} }) + if $options{'pkgnum'}; + + if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S' + || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) { + %domains = map { $_->svcnum => $_->domain } + map { qsearchs('svc_domain', { 'svcnum' => $_ }) } + split(',', $part_svc->part_svc_column('domsvc')->columnvalue); + }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) { + %domains = map { $_->svcnum => $_->domain } + map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) } + map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) } + qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum }); + }else{ + %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} ); + } + + if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') { + my $svc_domain = qsearchs('svc_domain', + { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } ); + if ( $svc_domain ) { + $domains{$svc_domain->svcnum} = $svc_domain->domain; + }else{ + warn "unknown svc_domain.svcnum for part_svc_column domsvc: ". + $part_svc->part_svc_column('domsvc')->columnvalue; + + } + } + + (%domains); +} + 1;