package FS::svc_acct;
use strict;
-use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
+use base qw( FS::svc_Domain_Mixin FS::svc_Common );
+use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
$dir_prefix @shells $usernamemin
$usernamemax $passwordmin $passwordmax
$username_ampersand $username_letter $username_letterfirst
use Fcntl qw(:flock);
use Date::Format;
use Crypt::PasswdMD5 1.2;
+use Digest::SHA1 'sha1_base64';
+use Digest::MD5 'md5_base64';
use Data::Dumper;
use Text::Template;
use Authen::Passphrase;
use FS::Record qw( qsearch qsearchs fields dbh dbdef );
use FS::Msgcat qw(gettext);
use FS::UI::bytecount;
+use FS::UI::Web;
use FS::part_pkg;
-use FS::svc_Common;
-use FS::cust_svc;
use FS::part_svc;
use FS::svc_acct_pop;
use FS::cust_main_invoice;
use FS::svc_domain;
+use FS::svc_pbx;
use FS::raddb;
use FS::queue;
use FS::radius_usergroup;
use FS::svc_www;
use FS::cdr;
-@ISA = qw( FS::svc_Common );
-
$DEBUG = 0;
$me = '[FS::svc_acct]';
@shells = $conf->config('shells');
$usernamemin = $conf->config('usernamemin') || 2;
$usernamemax = $conf->config('usernamemax');
- $passwordmin = $conf->config('passwordmin') || 6;
+ $passwordmin = $conf->config('passwordmin'); # || 6;
+ #blank->6, keep 0
+ $passwordmin = ( defined($passwordmin) && $passwordmin =~ /\d+/ )
+ ? $passwordmin
+ : 6;
$passwordmax = $conf->config('passwordmax') || 8;
$username_letter = $conf->exists('username-letter');
$username_letterfirst = $conf->exists('username-letterfirst');
=over 4
-=item svcnum - primary key (assigned automatcially for new accounts)
+=item svcnum
+
+Primary key (assigned automatcially for new accounts)
=item username
-=item _password - generated if blank
+=item _password
+
+generated if blank
+
+=item _password_encoding
-=item _password_encoding - plain, crypt, ldap (or empty for autodetection)
+plain, crypt, ldap (or empty for autodetection)
-=item sec_phrase - security phrase
+=item sec_phrase
-=item popnum - Point of presence (see L<FS::svc_acct_pop>)
+security phrase
+
+=item popnum
+
+Point of presence (see L<FS::svc_acct_pop>)
=item uid
=item gid
-=item finger - GECOS
+=item finger
+
+GECOS
+
+=item dir
-=item dir - set automatically if blank (and uid is not)
+set automatically if blank (and uid is not)
=item shell
-=item quota - (unimplementd)
+=item quota
-=item slipip - IP address
+=item slipip
-=item seconds -
+IP address
-=item upbytes -
+=item seconds
-=item downbytes -
+=item upbytes
-=item totalbytes -
+=item downbyte
-=item domsvc - svcnum from svc_domain
+=item totalbytes
-=item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
+=item domsvc
-=item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
+svcnum from svc_domain
+
+=item pbxsvc
+
+Optional svcnum from svc_pbx
+
+=item radius_I<Radius_Attribute>
+
+I<Radius-Attribute> (reply)
+
+=item rc_I<Radius_Attribute>
+
+I<Radius-Attribute> (check)
=back
disable_fixed => 1,
disable_select => 1,
},
+ 'password_selfchange' => { label => 'Password modification',
+ type => 'checkbox',
+ },
+ 'password_recover' => { label => 'Password recovery',
+ type => 'checkbox',
+ },
'quota' => {
- label => 'Quota',
+ label => 'Quota', #Mail storage limit
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'file_quota'=> {
+ label => 'File storage limit',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'file_maxnum'=> {
+ label => 'Number of files limit',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'file_maxsize'=> {
+ label => 'File size limit',
type => 'text',
disable_inventory => 1,
disable_select => 1,
select_key => 'svcnum',
select_label => 'domain',
disable_inventory => 1,
-
+ },
+ 'pbxsvc' => { label => 'PBX',
+ type => 'select-svc_pbx.html',
+ disable_inventory => 1,
+ disable_select => 1, #UI wonky, pry works otherwise
},
'usergroup' => {
label => 'RADIUS groups',
label => 'Last logout',
type => 'disabled',
},
+
+ 'cgp_aliases' => {
+ label => 'Communigate aliases',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ #settings
+ 'cgp_type'=> {
+ label => 'Communigate account type',
+ type => 'select',
+ select_list => [qw( MultiMailbox TextMailbox MailDirMailbox AGrade BGrade CGrade )],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_accessmodes' => {
+ label => 'Communigate enabled services',
+ type => 'communigate_pro-accessmodes',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_rulesallowed' => {
+ label => 'Allowed mail rules',
+ type => 'select',
+ select_list => [ '', 'No', 'Filter Only', 'All But Exec', 'Any' ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_rpopallowed' => { label => 'RPOP modifications',
+ type => 'checkbox',
+ },
+ 'cgp_mailtoall' => { label => 'Accepts mail to "all"',
+ type => 'checkbox',
+ },
+ 'cgp_addmailtrailer' => { label => 'Add trailer to sent mail',
+ type => 'checkbox',
+ },
+ #XXX archive messages, mailing lists
+
+ #preferences
+ 'cgp_deletemode' => {
+ label => 'Communigate message delete method',
+ type => 'select',
+ select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_emptytrash' => {
+ label => 'Communigate on logout remove trash',
+ type => 'text',
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_language' => {
+ label => 'Communigate language',
+ type => 'select',
+ select_list => [ '', qw( English Arabic Chinese Dutch French German Hebrew Italian Japanese Portuguese Russian Slovak Spanish Thai ) ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_timezone' => {
+ label => 'Communigate time zone',
+ type => 'select',
+ select_list => [ '',
+ 'HostOS',
+ '(+0100) Algeria/Congo',
+ '(+0200) Egypt/South Africa',
+ '(+0300) Saudi Arabia',
+ '(+0400) Oman',
+ '(+0500) Pakistan',
+ '(+0600) Bangladesh',
+ '(+0700) Thailand/Vietnam',
+ '(+0800) China/Malaysia',
+ '(+0900) Japan/Korea',
+ '(+1000) Queensland',
+ '(+1100) Micronesia',
+ '(+1200) Fiji',
+ '(+1300) Tonga/Kiribati',
+ '(+1400) Christmas Islands',
+ '(-0100) Azores/Cape Verde',
+ '(-0200) Fernando de Noronha',
+ '(-0300) Argentina/Uruguay',
+ '(-0400) Venezuela/Guyana',
+ '(-0500) Haiti/Peru',
+ '(-0600) Central America',
+ '(-0700) Arisona',
+ '(-0800) Adamstown',
+ '(-0900) Marquesas Islands',
+ '(-1000) Hawaii/Tahiti',
+ '(-1100) Samoa',
+ 'Asia/Afghanistan',
+ 'Asia/India',
+ 'Asia/Iran',
+ 'Asia/Iraq',
+ 'Asia/Israel',
+ 'Asia/Jordan',
+ 'Asia/Lebanon',
+ 'Asia/Syria',
+ 'Australia/Adelaide',
+ 'Australia/East',
+ 'Australia/NorthernTerritory',
+ 'Europe/Central',
+ 'Europe/Eastern',
+ 'Europe/Moscow',
+ 'Europe/Western',
+ 'GMT (+0000)',
+ 'Newfoundland',
+ 'NewZealand/Auckland',
+ 'NorthAmerica/Alaska',
+ 'NorthAmerica/Atlantic',
+ 'NorthAmerica/Central',
+ 'NorthAmerica/Eastern',
+ 'NorthAmerica/Mountain',
+ 'NorthAmerica/Pacific',
+ 'Russia/Ekaterinburg',
+ 'Russia/Irkutsk',
+ 'Russia/Kamchatka',
+ 'Russia/Krasnoyarsk',
+ 'Russia/Magadan',
+ 'Russia/Novosibirsk',
+ 'Russia/Vladivostok',
+ 'Russia/Yakutsk',
+ 'SouthAmerica/Brasil',
+ 'SouthAmerica/Chile',
+ 'SouthAmerica/Paraguay',
+ ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_skinname' => {
+ label => 'Communigate layout',
+ type => 'select',
+ select_list => [ '', '***', 'GoldFleece', 'Skin2' ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_prontoskinname' => {
+ label => 'Communigate Pronto style',
+ type => 'select',
+ select_list => [ '', 'Pronto', 'Pronto-darkflame', 'Pronto-steel', 'Pronto-twilight', ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+ 'cgp_sendmdnmode' => {
+ label => 'Communigate send read receipts',
+ type => 'select',
+ select_list => [ '', 'Never', 'Manually', 'Automatically' ],
+ disable_inventory => 1,
+ disable_select => 1,
+ },
+
+ #mail
+ #XXX vacation message, redirect all mail, mail rules
+ #XXX RPOP settings
+
},
};
}
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error = $self->check;
- return $error if $error;
-
- if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
- my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
- unless ( $cust_svc ) {
- $dbh->rollback if $oldAutoCommit;
- return "no cust_svc record found for svcnum ". $self->svcnum;
- }
- $self->pkgnum($cust_svc->pkgnum);
- $self->svcpart($cust_svc->svcpart);
- }
-
- # set usage fields and thresholds if unset but set in a package def
- if ( $self->pkgnum ) {
- my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
- my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
- if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
-
- my %values = $part_pkg->usage_valuehash;
- my $multiplier = $conf->exists('svc_acct-usage_threshold')
- ? 1 - $conf->config('svc_acct-usage_threshold')/100
- : 0.20; #doesn't matter
-
- foreach ( keys %values ) {
- next if $self->getfield($_);
- $self->setfield( $_, $values{$_} );
- $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
- if $conf->exists('svc_acct-usage_threshold');
- }
-
- }
- }
-
my @jobnums;
- $error = $self->SUPER::insert(
+ my $error = $self->SUPER::insert(
'jobnums' => \@jobnums,
'child_objects' => $self->child_objects,
%options,
''; #no error
}
+# set usage fields and thresholds if unset but set in a package def
+# AND the package already has a last bill date (otherwise they get double added)
+sub preinsert_hook_first {
+ my $self = shift;
+
+ return '' unless $self->pkgnum;
+
+ my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+ return '' unless $cust_pkg && $cust_pkg->last_bill;
+
+ my $part_pkg = $cust_pkg->part_pkg;
+ return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
+
+ my %values = $part_pkg->usage_valuehash;
+ my $multiplier = $conf->exists('svc_acct-usage_threshold')
+ ? 1 - $conf->config('svc_acct-usage_threshold')/100
+ : 0.20; #doesn't matter
+
+ foreach ( keys %values ) {
+ next if $self->getfield($_);
+ $self->setfield( $_, $values{$_} );
+ $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
+ if $conf->exists('svc_acct-usage_threshold');
+ }
+
+ ''; #no error
+}
+
=item delete
Deletes this account from the database. If there is an error, returns the
my $error = $self->ut_numbern('svcnum')
#|| $self->ut_number('domsvc')
- || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
+ || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
+ || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
|| $self->ut_textn('sec_phrase')
|| $self->ut_snumbern('seconds')
|| $self->ut_snumbern('upbytes')
|| $self->ut_snumbern('downbytes')
|| $self->ut_snumbern('totalbytes')
- || $self->ut_enum( '_password_encoding',
- [ '', qw( plain crypt ldap ) ]
- )
+ || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
+ || $self->ut_enum('password_selfchange', [ '', 'Y' ])
+ || $self->ut_enum('password_recover', [ '', 'Y' ])
+ || $self->ut_textn('cgp_accessmodes')
+ || $self->ut_alphan('cgp_type')
+ || $self->ut_textn('cgp_aliases' ) #well
+ #settings
+ || $self->ut_alphasn('cgp_rulesallowed')
+ || $self->ut_enum('cgp_rpopallowed', [ '', 'Y' ])
+ || $self->ut_enum('cgp_mailtoall', [ '', 'Y' ])
+ || $self->ut_enum('cgp_addmailtrailer', [ '', 'Y' ])
+ #preferences
+ || $self->ut_alphasn('cgp_deletemode')
+ || $self->ut_alphan('cgp_emptytrash')
+ || $self->ut_alphan('cgp_language')
+ || $self->ut_textn('cgp_timezone')
+ || $self->ut_textn('cgp_skinname')
+ || $self->ut_textn('cgp_prontoskinname')
+ || $self->ut_alphan('cgp_sendmdnmode')
+ #XXX vacation message, redirect all mail, mail rules
+ #XXX RPOP settings
;
return $error if $error;
or return "Illegal finger: ". $self->getfield('finger');
$self->setfield('finger', $1);
- $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
- $recref->{quota} = $1;
+ for (qw( quota file_quota file_maxsize )) {
+ $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
+ $recref->{$_} = $1;
+ }
+ $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
+ $recref->{file_maxnum} = $1;
unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
if ( $recref->{slipip} eq '' ) {
$self->ut_textn($_);
}
+ # First, if _password is blank, generate one and set default encoding.
+ if ( ! $recref->{_password} ) {
+ $error = $self->set_password('');
+ }
+ # But if there's a _password but no encoding, assume it's plaintext and
+ # set it to default encoding.
+ elsif ( ! $recref->{_password_encoding} ) {
+ $error = $self->set_password($recref->{_password});
+ }
+ return $error if $error;
+
+ # Next, check _password to ensure compliance with the encoding.
if ( $recref->{_password_encoding} eq 'ldap' ) {
if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
}
} elsif ( $recref->{_password_encoding} eq 'plain' ) {
-
- #generate a password if it is blank
- $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
- unless length( $recref->{_password} );
-
+ # Password randomization is now in set_password.
+ # Strip whitespace characters, check length requirements, etc.
if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
$recref->{_password} = $1;
} else {
if ( $password_noexclamation ) {
$recref->{_password} =~ /\!/ and return gettext('illegal_password');
}
+ }
+ else {
+ return "invalid password encoding ('".$recref->{_password_encoding}."'";
+ }
+ $self->SUPER::check;
- } else {
+}
- #carp "warning: _password_encoding unspecified\n";
- #generate a password if it is blank
- unless ( length( $recref->{_password} ) ) {
+sub _password_encryption {
+ my $self = shift;
+ my $encoding = lc($self->_password_encoding);
+ return if !$encoding;
+ return 'plain' if $encoding eq 'plain';
+ if($encoding eq 'crypt') {
+ my $pass = $self->_password;
+ $pass =~ s/^\*SUSPENDED\* //;
+ $pass =~ s/^!!?//;
+ return 'md5' if $pass =~ /^\$1\$/;
+ #return 'blowfish' if $self->_password =~ /^\$2\$/;
+ return 'des' if length($pass) == 13;
+ return;
+ }
+ if($encoding eq 'ldap') {
+ uc($self->_password) =~ /^\{([\w-]+)\}/;
+ return 'crypt' if $1 eq 'CRYPT' or $1 eq 'DES';
+ return 'plain' if $1 eq 'PLAIN' or $1 eq 'CLEARTEXT';
+ return 'md5' if $1 eq 'MD5';
+ return 'sha1' if $1 eq 'SHA' or $1 eq 'SHA-1';
+
+ return;
+ }
+ return;
+}
+
+sub get_cleartext_password {
+ my $self = shift;
+ if($self->_password_encryption eq 'plain') {
+ if($self->_password_encoding eq 'ldap') {
+ $self->_password =~ /\{\w+\}(.*)$/;
+ return $1;
+ }
+ else {
+ return $self->_password;
+ }
+ }
+ return;
+}
+
+
+=item set_password
- $recref->{_password} =
- join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
- $recref->{_password_encoding} = 'plain';
+Set the cleartext password for the account. If _password_encoding is set, the
+new password will be encoded according to the existing method (including
+encryption mode, if it can be determined). Otherwise,
+config('default-password-encoding') is used.
+If no password is supplied (or a zero-length password when minimum password length
+is >0), one will be generated randomly.
+
+=cut
+
+sub set_password {
+ my( $self, $pass ) = ( shift, shift );
+
+ warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
+ if $DEBUG;
+
+ my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
+ FS::Msgcat::_gettext('illegal_password_characters').
+ ": ". $pass;
+
+ my( $encoding, $encryption ) = ('', '');
+
+ if ( $self->_password_encoding ) {
+ $encoding = $self->_password_encoding;
+ # identify existing encryption method, try to use it.
+ $encryption = $self->_password_encryption;
+ if (!$encryption) {
+ # use the system default
+ undef $encoding;
+ }
+ }
+
+ if ( !$encoding ) {
+ # set encoding to system default
+ ($encoding, $encryption) =
+ split(/-/, lc($conf->config('default-password-encoding')));
+ $encoding ||= 'legacy';
+ $self->_password_encoding($encoding);
+ }
+
+ if ( $encoding eq 'legacy' ) {
+
+ # The legacy behavior from check():
+ # If the password is blank, randomize it and set encoding to 'plain'.
+ if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
+ $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+ $self->_password_encoding('plain');
} else {
-
- #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
- if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
- $recref->{_password} = $1.$3;
- $recref->{_password_encoding} = 'plain';
- } elsif ( $recref->{_password} =~
- /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/
- ) {
- $recref->{_password} = $1.$3;
- $recref->{_password_encoding} = 'crypt';
- } elsif ( $recref->{_password} eq '*' ) {
- $recref->{_password} = '*';
- $recref->{_password_encoding} = 'crypt';
- } elsif ( $recref->{_password} eq '!' ) {
- $recref->{_password_encoding} = 'crypt';
- $recref->{_password} = '!';
- } elsif ( $recref->{_password} eq '!!' ) {
- $recref->{_password} = '!!';
- $recref->{_password_encoding} = 'crypt';
+ # Prefix + valid-length password
+ if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
+ $pass = $1.$3;
+ $self->_password_encoding('plain');
+ # Prefix + crypt string
+ } elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
+ $pass = $1.$3;
+ $self->_password_encoding('crypt');
+ # Various disabled crypt passwords
+ } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
+ $self->_password_encoding('crypt');
} else {
- #return "Illegal password";
- return gettext('illegal_password'). " $passwordmin-$passwordmax ".
- FS::Msgcat::_gettext('illegal_password_characters').
- ": ". $recref->{_password};
+ return $failure;
}
-
}
+ $self->_password($pass);
+ return;
+
}
- $self->SUPER::check;
+ return $failure
+ if $passwordmin && length($pass) < $passwordmin
+ or $passwordmax && length($pass) > $passwordmax;
+
+ if ( $encoding eq 'crypt' ) {
+ if ($encryption eq 'md5') {
+ $pass = unix_md5_crypt($pass);
+ } elsif ($encryption eq 'des') {
+ $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
+ }
+ } elsif ( $encoding eq 'ldap' ) {
+ if ($encryption eq 'md5') {
+ $pass = md5_base64($pass);
+ } elsif ($encryption eq 'sha1') {
+ $pass = sha1_base64($pass);
+ } elsif ($encryption eq 'crypt') {
+ $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
+ }
+ # else $encryption eq 'plain', do nothing
+ $pass = '{'.uc($encryption).'}'.$pass;
+ }
+ # else encoding eq 'plain'
+
+ $self->_password($pass);
+ return;
}
=item _check_system
sub radius_password {
my $self = shift;
- my($pw_attrib, $password);
+ my $pw_attrib;
if ( $self->_password_encoding eq 'ldap' ) {
-
$pw_attrib = 'Password-With-Header';
- $password = $self->_password;
-
} elsif ( $self->_password_encoding eq 'crypt' ) {
-
$pw_attrib = 'Crypt-Password';
- $password = $self->_password;
-
} elsif ( $self->_password_encoding eq 'plain' ) {
-
- $pw_attrib = $radius_password; #Cleartext-Password? man rlm_pap
- $password = $self->_password;
-
+ $pw_attrib = $radius_password;
} else {
-
- $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
- $password = $self->_password;
-
+ $pw_attrib = length($self->_password) <= 12
+ ? $radius_password
+ : 'Crypt-Password';
}
- ($pw_attrib, $password);
+ ($pw_attrib, $self->_password);
}
$svc_domain->domain;
}
-=item svc_domain
-
-Returns the FS::svc_domain record for this account's domain (see
-L<FS::svc_domain>).
-
-=cut
-
-# FS::h_svc_acct has a history-aware svc_domain override
-
-sub svc_domain {
- my $self = shift;
- $self->{'_domsvc'}
- ? $self->{'_domsvc'}
- : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
-}
-
=item cust_svc
Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
( $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";
- }
- }
+
+ my $error = $self->_op_overlimit($action);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
}
+
}
if ( $conf->exists("svc_acct-usage_$action")
}
+sub _op_overlimit {
+ my( $self, $action ) = @_;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_pkg = $self->cust_svc->cust_pkg;
+
+ my $conf_overlimit =
+ $cust_pkg
+ ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
+ : $conf->config('overlimit_groups');
+
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+
+ my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
+ next unless $groups;
+
+ my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
+
+ my $other = new FS::svc_acct $self->hashref;
+ $other->usergroup( $gref );
+
+ my($new,$old);
+ if ($action eq 'suspend') {
+ $new = $other;
+ $old = $self;
+ } else { # $action eq 'unsuspend'
+ $new = $self;
+ $old = $other;
+ }
+
+ my $error = $part_export->export_replace($new, $old)
+ || $self->overlimit($action);
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error replacing radius groups: $error";
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
sub set_usage {
my( $self, $valueref, %options ) = @_;
#$self->snapshot; #not necessary, we retain the old values
#create an object with the updated usage values
my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
- #call exports
- my $error = $new->replace($self);
+ local($FS::Record::nowarn_identical) = 1;
+ my $error = $new->replace($self); #call exports
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error replacing: $error";
}
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")) {
- $error ||= $self->cust_svc->cust_pkg->unsuspend;
- }
+ my $error = '';
+
+ $error = $self->_op_overlimit('unsuspend')
+ if $self->overlimit;;
+
+ $error ||= $self->cust_svc->cust_pkg->unsuspend
+ if $conf->exists("svc_acct-usage_unsuspend");
+
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error unsuspending: $error";
}
+
}
warn "$me update successful; committing\n"
=back
+=head1 CLASS METHODS
+
+=over 4
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF. Valid parameters are
+
+=over 4
+
+=item domain
+
+=item domsvc
+
+=item unlinked
+
+=item agentnum
+
+=item pkgpart
+
+Arrayref of pkgparts
+
+=item pkgpart
+
+=item where
+
+Arrayref of additional WHERE clauses, will be ANDed together.
+
+=item order_by
+
+=item cust_fields
+
+=back
+
+=cut
+
+sub search {
+ my ($class, $params) = @_;
+
+ my @where = ();
+
+ # domain
+ if ( $params->{'domain'} ) {
+ my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
+ #preserve previous behavior & bubble up an error if $svc_domain not found?
+ push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+ }
+
+ # domsvc
+ if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
+ push @where, "domsvc = $1";
+ }
+
+ #unlinked
+ push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
+
+ #agentnum
+ if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
+ push @where, "agentnum = $1";
+ }
+
+ #custnum
+ if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
+ push @where, "custnum = $1";
+ }
+
+ #pkgpart
+ if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
+ #XXX untaint or sql quote
+ push @where,
+ 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
+ }
+
+ # popnum
+ if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
+ push @where, "popnum = $1";
+ }
+
+ # svcpart
+ if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
+ push @where, "svcpart = $1";
+ }
+
+
+ # here is the agent virtualization
+ #if ($params->{CurrentUser}) {
+ # my $access_user =
+ # qsearchs('access_user', { username => $params->{CurrentUser} });
+ #
+ # if ($access_user) {
+ # push @where, $access_user->agentnums_sql('table'=>'cust_main');
+ # }else{
+ # push @where, "1=0";
+ # }
+ #} else {
+ push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
+ 'table' => 'cust_main',
+ 'null_right' => 'View/link unlinked services',
+ );
+ #}
+
+ push @where, @{ $params->{'where'} } if $params->{'where'};
+
+ my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
+
+ my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+ my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
+ #if ( keys %svc_acct ) {
+ # $count_query .= ' WHERE '.
+ # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
+ # keys %svc_acct
+ # );
+ #}
+
+ my $sql_query = {
+ 'table' => 'svc_acct',
+ 'hashref' => {}, # \%svc_acct,
+ 'select' => join(', ',
+ 'svc_acct.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
+ ),
+ 'addl_from' => $addl_from,
+ 'extra_sql' => $extra_sql,
+ 'order_by' => $params->{'order_by'},
+ 'count_query' => $count_query,
+ };
+
+}
+
+=back
+
=head1 SUBROUTINES
=over 4
=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<pkgnum> I<svcpart>
-
-=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;
-