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;