summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/CGI.pm6
-rw-r--r--FS/FS/ClientAPI.pm44
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm136
-rw-r--r--FS/FS/ClientAPI/passwd.pm56
-rw-r--r--FS/FS/Conf.pm77
-rw-r--r--FS/FS/InitHandler.pm88
-rw-r--r--FS/FS/Record.pm42
-rw-r--r--FS/FS/UID.pm7
-rw-r--r--FS/FS/cust_bill.pm186
-rw-r--r--FS/FS/cust_bill_event.pm15
-rw-r--r--FS/FS/cust_credit.pm6
-rw-r--r--FS/FS/cust_main.pm395
-rw-r--r--FS/FS/cust_main_county.pm2
-rw-r--r--FS/FS/cust_pay.pm6
-rw-r--r--FS/FS/cust_pkg.pm4
-rw-r--r--FS/FS/domain_record.pm8
-rw-r--r--FS/FS/part_export.pm266
-rw-r--r--FS/FS/part_export/bind.pm7
-rw-r--r--FS/FS/part_export/bind_slave.pm7
-rw-r--r--FS/FS/part_export/bsdshell.pm47
-rw-r--r--FS/FS/part_export/domain_shellcommands.pm115
-rw-r--r--FS/FS/part_export/http.pm88
-rw-r--r--FS/FS/part_export/infostreet.pm140
-rw-r--r--FS/FS/part_export/null.pm13
-rw-r--r--FS/FS/part_export/shellcommands.pm54
-rw-r--r--FS/FS/part_export/shellcommands_withdomain.pm7
-rw-r--r--FS/FS/part_export/sqlmail.pm111
-rw-r--r--FS/FS/part_export/sysvshell.pm7
-rw-r--r--FS/FS/part_export/textradius.pm138
-rw-r--r--FS/FS/part_export/vpopmail.pm74
-rw-r--r--FS/FS/part_export/www_shellcommands.pm114
-rw-r--r--FS/FS/part_export_option.pm2
-rw-r--r--FS/FS/queue.pm23
-rw-r--r--FS/FS/svc_Common.pm161
-rw-r--r--FS/FS/svc_acct.pm400
-rw-r--r--FS/FS/svc_domain.pm8
-rw-r--r--FS/FS/svc_forward.pm4
-rw-r--r--FS/FS/svc_www.pm74
-rw-r--r--FS/MANIFEST28
-rw-r--r--FS/bin/freeside-addoutsource24
-rw-r--r--FS/bin/freeside-addoutsourceuser15
-rw-r--r--FS/bin/freeside-adduser16
-rwxr-xr-xFS/bin/freeside-daily9
-rw-r--r--FS/bin/freeside-queued58
-rw-r--r--FS/bin/freeside-reexport62
-rwxr-xr-xFS/bin/freeside-setup1038
-rwxr-xr-xFS/bin/freeside-sqlradius-reset4
-rw-r--r--FS/t/ClientAPI.t5
-rw-r--r--FS/t/InitHandler.t5
-rw-r--r--FS/t/part_export-bind.t5
-rw-r--r--FS/t/part_export-bind_slave.t5
-rw-r--r--FS/t/part_export-domain_shellcommands.t5
-rw-r--r--FS/t/part_export-http.t5
-rw-r--r--FS/t/part_export-null.t5
-rw-r--r--FS/t/part_export-shellcommands_withdomain.t5
-rw-r--r--FS/t/part_export-sqlmail.t5
-rw-r--r--FS/t/part_export-sysvshell.t5
-rw-r--r--FS/t/part_export-www_shellcommands.t5
58 files changed, 3851 insertions, 396 deletions
diff --git a/FS/FS/CGI.pm b/FS/FS/CGI.pm
index 190c0aa37..e44ebcc0a 100644
--- a/FS/FS/CGI.pm
+++ b/FS/FS/CGI.pm
@@ -1,7 +1,7 @@
package FS::CGI;
use strict;
-use vars qw(@EXPORT_OK @ISA @header);
+use vars qw(@EXPORT_OK @ISA);
use Exporter;
use CGI;
use URI::URL;
@@ -253,7 +253,7 @@ sub small_custview {
my $html = 'Customer #<B>'. $cust_main->custnum. '</B>'.
ntable('#e8e8e8'). '<TR><TD>'. ntable("#cccccc",2).
- '<TR><TD ALIGN="right" VALIGN="top">Billing</TD><TD BGCOLOR="#ffffff">'.
+ '<TR><TD ALIGN="right" VALIGN="top">Billing<BR>Address</TD><TD BGCOLOR="#ffffff">'.
$cust_main->getfield('last'). ', '. $cust_main->first. '<BR>';
$html .= $cust_main->company. '<BR>' if $cust_main->company;
@@ -270,7 +270,7 @@ sub small_custview {
my $pre = $cust_main->ship_last ? 'ship_' : '';
$html .= '<TD>'. ntable("#cccccc",2).
- '<TR><TD ALIGN="right" VALIGN="top">Service</TD><TD BGCOLOR="#ffffff">'.
+ '<TR><TD ALIGN="right" VALIGN="top">Service<BR>Address</TD><TD BGCOLOR="#ffffff">'.
$cust_main->get("${pre}last"). ', '.
$cust_main->get("${pre}first"). '<BR>';
$html .= $cust_main->get("${pre}company"). '<BR>'
diff --git a/FS/FS/ClientAPI.pm b/FS/FS/ClientAPI.pm
new file mode 100644
index 000000000..f7b8eb028
--- /dev/null
+++ b/FS/FS/ClientAPI.pm
@@ -0,0 +1,44 @@
+package FS::ClientAPI;
+
+use strict;
+use vars qw(%handler);
+
+%handler = ();
+
+#find modules
+foreach my $INC ( @INC ) {
+ foreach my $file ( glob("$INC/FS/ClientAPI/*") ) {
+ $file =~ /\/(\w+)\.pm$/ or do {
+ warn "unrecognized ClientAPI file: $file";
+ next
+ };
+ my $mod = $1;
+ #warn "using FS::ClientAPI::$mod";
+ eval "use FS::ClientAPI::$mod;";
+ die "error using FS::ClientAPI::$mod: $@" if $@;
+ }
+}
+
+#(sub for modules)
+sub register_handlers {
+ my $self = shift;
+ my %new_handlers = @_;
+ foreach my $key ( keys %new_handlers ) {
+ warn "WARNING: redefining sub $key" if exists $handler{$key};
+ #warn "registering $key";
+ $handler{$key} = $new_handlers{$key};
+ }
+}
+
+#---
+
+sub dispatch {
+ my ( $self, $name ) = ( shift, shift );
+ my $sub = $handler{$name}
+ or die "unknown FS::ClientAPI sub $name (known: ". join(" ", keys %handler );
+ #or die "unknown FS::ClientAPI sub $name";
+ &{$sub}(@_);
+}
+
+1;
+
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
new file mode 100644
index 000000000..674785524
--- /dev/null
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -0,0 +1,136 @@
+package FS::ClientAPI::MyAccount;
+
+use strict;
+use vars qw($cache);
+use Digest::MD5 qw(md5_hex);
+use Date::Format;
+use Cache::SharedMemoryCache; #store in db?
+use FS::CGI qw(small_custview); #doh
+use FS::Conf;
+use FS::Record qw(qsearchs);
+use FS::svc_acct;
+use FS::svc_domain;
+use FS::cust_main;
+use FS::cust_bill;
+
+use FS::ClientAPI; #hmm
+FS::ClientAPI->register_handlers(
+ 'MyAccount/login' => \&login,
+ 'MyAccount/customer_info' => \&customer_info,
+ 'MyAccount/invoice' => \&invoice,
+);
+
+#store in db?
+my $cache = new Cache::SharedMemoryCache();
+
+#false laziness w/FS::ClientAPI::passwd::passwd (needs to handle encrypted pw)
+sub login {
+ my $p = shift;
+
+ my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
+ or return { error => "Domain not found" };
+
+ my $svc_acct =
+ ( length($p->{'password'}) < 13
+ && qsearchs( 'svc_acct', { 'username' => $p->{'username'},
+ 'domsvc' => $svc_domain->svcnum,
+ '_password' => $p->{'password'} } )
+ )
+ || qsearchs( 'svc_acct', { 'username' => $p->{'username'},
+ 'domsvc' => $svc_domain->svcnum,
+ '_password' => $p->{'password'} } );
+
+ unless ( $svc_acct ) { return { error => 'Incorrect password.' } }
+
+ my $session = {
+ 'svcnum' => $svc_acct->svcnum,
+ };
+
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ if ( $cust_pkg ) {
+ my $cust_main = $cust_pkg->cust_main;
+ $session->{'custnum'} = $cust_main->custnum;
+ }
+
+ my $session_id;
+ do {
+ $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
+ } until ( ! defined $cache->get($session_id) ); #just in case
+
+ $cache->set( $session_id, $session, '1 hour' );
+
+ return { 'error' => '',
+ 'session_id' => $session_id,
+ };
+}
+
+sub customer_info {
+ my $p = shift;
+ my $session = $cache->get($p->{'session_id'})
+ or return { 'error' => "Can't resume session" }; #better error message
+
+ my %return;
+
+ my $custnum = $session->{'custnum'};
+
+ if ( $custnum ) { #customer record
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ $return{balance} = $cust_main->balance;
+
+ my @open = map {
+ {
+ invnum => $_->invnum,
+ date => time2str("%b %o, %Y", $_->_date),
+ owed => $_->owed,
+ };
+ } $cust_main->open_cust_bill;
+ $return{open_invoices} = \@open;
+
+ my $conf = new FS::Conf;
+ $return{small_custview} =
+ small_custview( $cust_main, $conf->config('defaultcountry') );
+
+ $return{name} = $cust_main->first. ' '. $cust_main->get('last');
+
+ } else { #no customer record
+
+ my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
+ or die "unknown svcnum";
+ $return{name} = $svc_acct->email;
+
+ }
+
+
+ return { 'error' => '',
+ 'custnum' => $custnum,
+ %return,
+ };
+
+}
+
+sub invoice {
+ my $p = shift;
+ my $session = $cache->get($p->{'session_id'})
+ or return { 'error' => "Can't resume session" }; #better error message
+
+ my $custnum = $session->{'custnum'};
+
+ my $invnum = $p->{'invnum'};
+
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum,
+ 'custnum' => $custnum } )
+ or return { 'error' => "Can't find invnum" };
+
+ #my %return;
+
+ return { 'error' => '',
+ 'invnum' => $invnum,
+ 'invoice_text' => join('', $cust_bill->print_text ),
+ };
+
+}
+
+
diff --git a/FS/FS/ClientAPI/passwd.pm b/FS/FS/ClientAPI/passwd.pm
new file mode 100644
index 000000000..29606227d
--- /dev/null
+++ b/FS/FS/ClientAPI/passwd.pm
@@ -0,0 +1,56 @@
+package FS::ClientAPI::passwd;
+
+use strict;
+use FS::Record qw(qsearchs);
+use FS::svc_acct;
+#use FS::svc_domain;
+
+use FS::ClientAPI; #hmm
+FS::ClientAPI->register_handlers(
+ 'passwd/passwd' => \&passwd,
+ 'passwd/chfn' => \&chfn,
+ 'passwd/chsh' => \&chsh,
+);
+
+sub passwd {
+ my $packet = shift;
+
+ #my $domain = qsearchs('svc_domain', { 'domain' => $packet->{'domain'} } )
+ # or return { error => "Domain $domain not found" };
+
+ my $old_password = $packet->{'old_password'};
+ my $new_password = $packet->{'new_password'};
+ my $new_gecos = $packet->{'new_gecos'};
+ my $new_shell = $packet->{'new_shell'};
+
+#false laziness w/FS::ClientAPI::MyAccount::login (needs to handle encrypted pw)
+ my $svc_acct =
+ ( length($old_password) < 13
+ && qsearchs( 'svc_acct', { 'username' => $packet->{'username'},
+ #'domsvc' => $svc_domain->svcnum,
+ '_password' => $old_password } )
+ )
+ || qsearchs( 'svc_acct', { 'username' => $packet->{'username'},
+ #'domsvc' => $svc_domain->svcnum,
+ '_password' => $old_password } );
+
+ unless ( $svc_acct ) { return { error => 'Incorrect password.' } }
+
+ my %hash = $svc_acct->hash;
+ my $new_svc_acct = new FS::svc_acct ( \%hash );
+ $new_svc_acct->setfield('_password', $new_password )
+ if $new_password && $new_password ne $old_password;
+ $new_svc_acct->setfield('finger',$new_gecos) if $new_gecos;
+ $new_svc_acct->setfield('shell',$new_shell) if $new_shell;
+ my $error = $new_svc_acct->replace($svc_acct);
+
+ return { error => $error };
+
+}
+
+sub chfn {}
+
+sub chsh {}
+
+1;
+
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 126461763..e93eaf3fc 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -221,8 +221,8 @@ httemplate/docs/config.html
{
'key' => 'apacheroot',
- 'section' => 'apache',
- 'description' => 'The directory containing Apache virtual hosts',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add a <i>www_shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead. The directory containing Apache virtual hosts',
'type' => 'text',
},
@@ -235,8 +235,8 @@ httemplate/docs/config.html
{
'key' => 'apachemachine',
- 'section' => 'apache',
- 'description' => 'A machine with the apacheroot directory and user home directories. The existance of this file enables setup of virtual host directories, and, in conjunction with the `home\' configuration file, symlinks into user home directories.',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add a <i>www_shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead. A machine with the apacheroot directory and user home directories. The existance of this file enables setup of virtual host directories, and, in conjunction with the `home\' configuration file, symlinks into user home directories.',
'type' => 'text',
},
@@ -249,15 +249,15 @@ httemplate/docs/config.html
{
'key' => 'bindprimary',
- 'section' => 'BIND',
- 'description' => 'Your BIND primary nameserver. This enables export of /var/named/named.conf and zone files into /var/named',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add a <i>bind</i> <a href="../browse/part_export.cgi">export</a> instead. Your BIND primary nameserver. This enables export of /var/named/named.conf and zone files into /var/named',
'type' => 'text',
},
{
'key' => 'bindsecondaries',
- 'section' => 'BIND',
- 'description' => 'Your BIND secondary nameservers, one per line. This enables export of /var/named/named.conf',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add a <i>bind_slave</i> <a href="../browse/part_export.cgi">export</a> instead. Your BIND secondary nameservers, one per line. This enables export of /var/named/named.conf',
'type' => 'textarea',
},
@@ -277,8 +277,8 @@ httemplate/docs/config.html
{
'key' => 'bsdshellmachines',
- 'section' => 'shell',
- 'description' => 'Your BSD flavored shell (and mail) machines, one per line. This enables export of `/etc/passwd\' and `/etc/master.passwd\'.',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add a <i>bsdshell</i> <a href="../browse/part_export.cgi">export</a> instead. Your BSD flavored shell (and mail) machines, one per line. This enables export of `/etc/passwd\' and `/etc/master.passwd\'.',
'type' => 'textarea',
},
@@ -348,7 +348,7 @@ httemplate/docs/config.html
{
'key' => 'editreferrals',
'section' => 'UI',
- 'description' => 'Enable referral modification for existing customers',
+ 'description' => 'Enable advertising source modification for existing customers',
'type' => 'checkbox',
},
@@ -404,28 +404,28 @@ httemplate/docs/config.html
{
'key' => 'icradiusmachines',
'section' => 'deprecated',
- 'description' => '<b>DEPRECATED</b>, add a <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to enable radcheck and radreply table population - by default in the Freeside database, or in the database specified by the <a href="http://rootwood.haze.st/aspside/config/config-view.cgi#icradius_secrets">icradius_secrets</a> config option (the radcheck and radreply tables needs to be created manually). You do not need to use MySQL for your Freeside database to export to an ICRADIUS/FreeRADIUS MySQL database with this option. <blockquote><b>ADDITIONAL DEPRECATED FUNCTIONALITY</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - your <a href="ftp://ftp.cheapnet.net/pub/icradius">ICRADIUS</a> machines or <a href="http://www.freeradius.org/">FreeRADIUS</a> (with MySQL authentication) machines, one per line. Machines listed in this file will have the radcheck table exported to them. Each line should contain four items, separted by whitespace: machine name, MySQL database name, MySQL username, and MySQL password. For example: <CODE>"radius.isp.tld&nbsp;radius_db&nbsp;radius_user&nbsp;passw0rd"</CODE></blockquote>',
+ 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to enable radcheck and radreply table population - by default in the Freeside database, or in the database specified by the <a href="http://rootwood.haze.st/aspside/config/config-view.cgi#icradius_secrets">icradius_secrets</a> config option (the radcheck and radreply tables needs to be created manually). You do not need to use MySQL for your Freeside database to export to an ICRADIUS/FreeRADIUS MySQL database with this option. <blockquote><b>ADDITIONAL DEPRECATED FUNCTIONALITY</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - your <a href="ftp://ftp.cheapnet.net/pub/icradius">ICRADIUS</a> machines or <a href="http://www.freeradius.org/">FreeRADIUS</a> (with MySQL authentication) machines, one per line. Machines listed in this file will have the radcheck table exported to them. Each line should contain four items, separted by whitespace: machine name, MySQL database name, MySQL username, and MySQL password. For example: <CODE>"radius.isp.tld&nbsp;radius_db&nbsp;radius_user&nbsp;passw0rd"</CODE></blockquote>',
'type' => [qw( checkbox textarea )],
},
{
'key' => 'icradius_mysqldest',
'section' => 'deprecated',
- 'description' => '<b>DEPRECATED</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - Destination directory for the MySQL databases, on the ICRADIUS/FreeRADIUS machines. Defaults to "/usr/local/var/".',
+ 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> https://billing.crosswind.net/freeside/browse/part_export.cgi">export</a> instead. Used to be the destination directory for the MySQL databases, on the ICRADIUS/FreeRADIUS machines. Defaults to "/usr/local/var/".',
'type' => 'text',
},
{
'key' => 'icradius_mysqlsource',
'section' => 'deprecated',
- 'description' => '<b>DEPRECATED</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - Source directory for for the MySQL radcheck table files, on the Freeside machine. Defaults to "/usr/local/var/freeside".',
+ 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> https://billing.crosswind.net/freeside/browse/part_export.cgi">export</a> instead. Used to be the source directory for for the MySQL radcheck table files, on the Freeside machine. Defaults to "/usr/local/var/freeside".',
'type' => 'text',
},
{
'key' => 'icradius_secrets',
'section' => 'deprecated',
- 'description' => '<b>DEPRECATED</b>, add <i>sqlradius</i> exports to <a href="../browse/part_svc">Service definitions</a> instead. This option used to specify a database for ICRADIUS/FreeRADIUS export. Three lines: DBI data source, username and password.',
+ 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> https://billing.crosswind.net/freeside/browse/part_export.cgi">export</a> instead. This option used to specify a database for ICRADIUS/FreeRADIUS export. Three lines: DBI data source, username and password.',
'type' => 'textarea',
},
@@ -506,8 +506,8 @@ httemplate/docs/config.html
{
'key' => 'nismachines',
- 'section' => 'shell',
- 'description' => 'Your NIS master (not slave master) machines, one per line. This enables export of `/etc/global/passwd\' and `/etc/global/shadow\'.',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>. Your NIS master (not slave master) machines, one per line. This enables export of `/etc/global/passwd\' and `/etc/global/shadow\'.',
'type' => 'textarea',
},
@@ -534,8 +534,8 @@ httemplate/docs/config.html
{
'key' => 'radiusmachines',
- 'section' => 'radius',
- 'description' => 'Your RADIUS authentication machines, one per line. This enables export of `/etc/raddb/users\'.',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to export to be: your RADIUS authentication machines, one per line. This enables export of `/etc/raddb/users\'.',
'type' => 'textarea',
},
@@ -633,8 +633,8 @@ httemplate/docs/config.html
{
'key' => 'shellmachines',
- 'section' => 'shell',
- 'description' => 'Your Linux and System V flavored shell (and mail) machines, one per line. This enables export of `/etc/passwd\' and `/etc/shadow\' files.',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, add a <i>sysvshell</i> <a href="../browse/part_export.cgi">export</a> instead. Your Linux and System V flavored shell (and mail) machines, one per line. This enables export of `/etc/passwd\' and `/etc/shadow\' files.',
'type' => 'textarea',
},
@@ -717,15 +717,15 @@ httemplate/docs/config.html
{
'key' => 'radiusprepend',
- 'section' => 'radius',
- 'description' => 'The contents will be prepended to the top of the RADIUS users file (text exports only).',
+ 'section' => 'deprecated',
+ 'description' => '<b>DEPRECATED</b>, real-time text radius now edits an existing file in place - just (turn off freeside-queued and) edit your RADIUS users file directly. The contents used to be be prepended to the top of the RADIUS users file (text exports only).',
'type' => 'textarea',
},
{
'key' => 'textradiusprepend',
'section' => 'deprecated',
- 'description' => '<b>DEPRECATED</b>, use RADIUS check attributes instead. This option will be removed soon. The contents will be prepended to the first line of a user\'s RADIUS entry in text exports.',
+ 'description' => '<b>DEPRECATED</b>, use RADIUS check attributes instead. The contents used to be prepended to the first line of a user\'s RADIUS entry in text exports.',
'type' => 'text',
},
@@ -937,6 +937,35 @@ httemplate/docs/config.html
'type' => 'checkbox',
},
+ {
+ 'key' => 'welcome_email',
+ 'section' => '',
+ 'description' => 'Template file for welcome email. Welcome emails are sent to the customer email invoice destination(s) each time a svc_acct record is created. See the <a href="http://search.cpan.org/doc/MJD/Text-Template-1.42/Template.pm">Text::Template</a> documentation for details on the template substitution language. The following variables are available: <code>$username</code>, <code>$password</code>, <code>$first</code>, <code>$last</code> and <code>$pkg</code>.',
+ 'type' => 'textarea',
+ },
+
+ {
+ 'key' => 'welcome_email-from',
+ 'section' => '',
+ 'description' => 'From: address header for welcome email',
+ 'type' => 'text',
+ },
+
+ {
+ 'key' => 'welcome_email-subject',
+ 'section' => '',
+ 'description' => 'Subject: header for welcome email',
+ 'type' => 'text',
+ },
+
+ {
+ 'key' => 'welcome_email-mimetype',
+ 'section' => '',
+ 'description' => 'MIME type for welcome email',
+ 'type' => 'select',
+ 'select_enum' => [ 'text/plain', 'text/html' ],
+ },
+
);
1;
diff --git a/FS/FS/InitHandler.pm b/FS/FS/InitHandler.pm
new file mode 100644
index 000000000..87f507c22
--- /dev/null
+++ b/FS/FS/InitHandler.pm
@@ -0,0 +1,88 @@
+package FS::InitHandler;
+
+use strict;
+use vars qw($DEBUG);
+use FS::UID qw(adminsuidsetup);
+use FS::Record;
+
+$DEBUG = 1;
+
+sub handler {
+
+ use Date::Format;
+ use Date::Parse;
+ use Tie::IxHash;
+ use HTML::Entities;
+ use IO::Handle;
+ use IO::File;
+ use String::Approx;
+ use HTML::Widgets::SelectLayers 0.02;
+ #use FS::UID;
+ #use FS::Record;
+ use FS::Conf;
+ use FS::CGI;
+ use FS::Msgcat;
+
+ use FS::agent;
+ use FS::agent_type;
+ use FS::domain_record;
+ use FS::cust_bill;
+ use FS::cust_bill_pay;
+ use FS::cust_credit;
+ use FS::cust_credit_bill;
+ use FS::cust_main;
+ use FS::cust_main_county;
+ use FS::cust_pay;
+ use FS::cust_pkg;
+ use FS::cust_refund;
+ use FS::cust_svc;
+ use FS::nas;
+ use FS::part_bill_event;
+ use FS::part_pkg;
+ use FS::part_referral;
+ use FS::part_svc;
+ use FS::pkg_svc;
+ use FS::port;
+ use FS::queue;
+ use FS::raddb;
+ use FS::session;
+ use FS::svc_acct;
+ use FS::svc_acct_pop;
+ use FS::svc_acct_sm;
+ use FS::svc_domain;
+ use FS::svc_forward;
+ use FS::svc_www;
+ use FS::type_pkgs;
+ use FS::part_export;
+ use FS::part_export_option;
+ use FS::export_svc;
+ use FS::msgcat;
+
+ warn "[FS::InitHandler] handler called\n" if $DEBUG;
+
+ #this is sure to be broken on freebsd
+ $> = $FS::UID::freeside_uid;
+
+ open(MAPSECRETS,"<$FS::UID::conf_dir/mapsecrets")
+ or die "can't read $FS::UID::conf_dir/mapsecrets: $!";
+
+ my %seen;
+ while (<MAPSECRETS>) {
+ next if /^\s*(#|$)/;
+ /^([\w\-\.]+)\s(.*)$/
+ or do { warn "strange line in mapsecrets: $_"; next; };
+ my($user, $datasrc) = ($1, $2);
+ next if $seen{$datasrc}++;
+ warn "[FS::InitHandler] preloading $datasrc for $user\n" if $DEBUG;
+ adminsuidsetup($user);
+ }
+
+ close MAPSECRETS;
+
+ #lalala probably broken on freebsd
+ ($<, $>) = ($>, $<);
+ $< = 0;
+
+}
+
+1;
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index f7c3a41c8..e6126a13b 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -2,7 +2,7 @@ package FS::Record;
use strict;
use vars qw( $dbdef_file $dbdef $setup_hack $AUTOLOAD @ISA @EXPORT_OK $DEBUG
- $me );
+ $me %dbdef_cache );
use subs qw(reload_dbdef);
use Exporter;
use Carp qw(carp cluck croak confess);
@@ -132,15 +132,8 @@ sub new {
my $hashref = $self->{'Hash'} = shift;
- foreach my $field ( $self->fields ) {
- $hashref->{$field}='' unless defined $hashref->{$field};
- #trim the '$' and ',' from money fields for Pg (belong HERE?)
- #(what about Pg i18n?)
- if ( driver_name =~ /^Pg$/i
- && $self->dbdef_table->column($field)->type eq 'money' ) {
- ${$hashref}{$field} =~ s/^\$//;
- ${$hashref}{$field} =~ s/\,//;
- }
+ foreach my $field ( grep !defined($hashref->{$_}), $self->fields ) {
+ $hashref->{$field}='';
}
$self->_cache($hashref, shift) if $self->can('_cache') && @_;
@@ -250,7 +243,7 @@ sub qsearch {
}
$statement .= " $extra_sql" if defined($extra_sql);
- warn "[debug]$me $statement\n" if $DEBUG;
+ warn "[debug]$me $statement\n" if $DEBUG > 1;
my $sth = $dbh->prepare($statement)
or croak "$dbh->errstr doing $statement";
@@ -502,13 +495,13 @@ sub insert {
join( ', ', @values ).
")"
;
- warn "[debug]$me $statement\n" if $DEBUG;
+ warn "[debug]$me $statement\n" if $DEBUG > 1;
my $sth = dbh->prepare($statement) or return dbh->errstr;
my $h_sth;
if ( defined $dbdef->table('h_'. $self->table) ) {
my $h_statement = $self->_h_statement('insert');
- warn "[debug]$me $h_statement\n" if $DEBUG;
+ warn "[debug]$me $h_statement\n" if $DEBUG > 2;
$h_sth = dbh->prepare($h_statement) or return dbh->errstr;
} else {
$h_sth = '';
@@ -562,13 +555,13 @@ sub delete {
? ( $self->dbdef_table->primary_key)
: $self->fields
);
- warn "[debug]$me $statement\n" if $DEBUG;
+ warn "[debug]$me $statement\n" if $DEBUG > 1;
my $sth = dbh->prepare($statement) or return dbh->errstr;
my $h_sth;
if ( defined $dbdef->table('h_'. $self->table) ) {
my $h_statement = $self->_h_statement('delete');
- warn "[debug]$me $h_statement\n" if $DEBUG;
+ warn "[debug]$me $h_statement\n" if $DEBUG > 2;
$h_sth = dbh->prepare($h_statement) or return dbh->errstr;
} else {
$h_sth = '';
@@ -647,13 +640,13 @@ sub replace {
} ( $primary_key ? ( $primary_key ) : $old->fields )
)
;
- warn "[debug]$me $statement\n" if $DEBUG;
+ warn "[debug]$me $statement\n" if $DEBUG > 1;
my $sth = dbh->prepare($statement) or return dbh->errstr;
my $h_old_sth;
if ( defined $dbdef->table('h_'. $old->table) ) {
my $h_old_statement = $old->_h_statement('replace_old');
- warn "[debug]$me $h_old_statement\n" if $DEBUG;
+ warn "[debug]$me $h_old_statement\n" if $DEBUG > 2;
$h_old_sth = dbh->prepare($h_old_statement) or return dbh->errstr;
} else {
$h_old_sth = '';
@@ -662,7 +655,7 @@ sub replace {
my $h_new_sth;
if ( defined $dbdef->table('h_'. $new->table) ) {
my $h_new_statement = $new->_h_statement('replace_new');
- warn "[debug]$me $h_new_statement\n" if $DEBUG;
+ warn "[debug]$me $h_new_statement\n" if $DEBUG > 2;
$h_new_sth = dbh->prepare($h_new_statement) or return dbh->errstr;
} else {
$h_new_sth = '';
@@ -735,7 +728,7 @@ sub unique {
my($self,$field) = @_;
my($table)=$self->table;
- croak("&FS::UID::checkruid failed") unless &checkruid;
+ #croak("&FS::UID::checkruid failed") unless &checkruid;
croak "Unique called on field $field, but it is ",
$self->getfield($field),
@@ -1130,8 +1123,15 @@ I<$FS::Record::setup_hack> is true. Returns a DBIx::DBSchema object.
sub reload_dbdef {
my $file = shift || $dbdef_file;
- $dbdef = load DBIx::DBSchema $file
- or die "can't load database schema from $file";
+
+ unless ( exists $dbdef_cache{$file} ) {
+ warn "[debug]$me loading dbdef for $file\n" if $DEBUG;
+ $dbdef_cache{$file} = DBIx::DBSchema->load( $file )
+ or die "can't load database schema from $file";
+ } else {
+ warn "[debug]$me re-using cached dbdef for $file\n" if $DEBUG;
+ }
+ $dbdef = $dbdef_cache{$file};
}
=item dbdef
diff --git a/FS/FS/UID.pm b/FS/FS/UID.pm
index d34d28e06..8934d49fc 100644
--- a/FS/FS/UID.pm
+++ b/FS/FS/UID.pm
@@ -92,6 +92,7 @@ sub forksuidsetup {
foreach ( keys %callback ) {
&{$callback{$_}};
+ # breaks multi-database installs # delete $callback{$_}; #run once
}
$dbh;
@@ -170,7 +171,9 @@ Returns the current Freeside user.
=cut
sub getotaker {
- $user;
+ #$user;
+ #stupid kludge until schema otaker fields are not 8 chars
+ substr($user,0,8);
}
=item cgisetotaker
@@ -255,7 +258,7 @@ coderef into the hash %FS::UID::callback :
=head1 VERSION
-$Id: UID.pm,v 1.14 2002-02-23 07:00:21 ivan Exp $
+$Id: UID.pm,v 1.19 2002-08-29 06:02:52 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index e2705fd83..5e041ea59 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -11,6 +11,7 @@ use Date::Format;
use Mail::Internet 1.44;
use Mail::Header;
use Text::Template;
+use FS::UID qw( datasrc );
use FS::Record qw( qsearch qsearchs );
use FS::cust_main;
use FS::cust_bill_pkg;
@@ -369,9 +370,9 @@ emails or print. See L<FS::cust_main_invoice>.
sub send {
my($self,$template) = @_;
-
- #my @print_text = $cust_bill->print_text; #( date )
+ my @print_text = $self->print_text('', $template);
my @invoicing_list = $self->cust_main->invoicing_list;
+
if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
#false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
#$ENV{SMTPHOSTS} = $smtpmachine;
@@ -386,7 +387,7 @@ sub send {
] );
my $message = new Mail::Internet (
'Header' => $header,
- 'Body' => [ $self->print_text('', $template) ], #( date)
+ 'Body' => [ @print_text ], #( date)
);
$!=0;
$message->smtpsend( Host => $smtpmachine )
@@ -395,11 +396,12 @@ sub send {
" to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
" via server $smtpmachine with SMTP: $!";
- #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
- } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
+ }
+
+ if ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { #postal
open(LPR, "|$lpr")
or return "Can't open pipe to $lpr: $!";
- print LPR $self->print_text; #( date )
+ print LPR @print_text;
close LPR
or return $! ? "Error closing $lpr: $!"
: "Exit status $? from $lpr";
@@ -409,6 +411,154 @@ sub send {
}
+=item send_csv OPTIONS
+
+Sends invoice as a CSV data-file to a remote host with the specified protocol.
+
+Options are:
+
+protocol - currently only "ftp"
+server
+username
+password
+dir
+
+The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
+and YYMMDDHHMMSS is a timestamp.
+
+The fields of the CSV file is as follows:
+
+record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+
+=over 4
+
+=item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
+
+If B<record_type> is C<cust_bill>, this is a primary invoice record. The
+last five fields (B<pkg> through B<edate>) are irrelevant, and all other
+fields are filled in.
+
+If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
+first two fields (B<record_type> and B<invnum>) and the last five fields
+(B<pkg> through B<edate>) are filled in.
+
+=item invnum - invoice number
+=item custnum - customer number
+=item _date - invoice date
+=item charged - total invoice amount
+=item first - customer first name
+=item last - customer first name
+=item company - company name
+=item address1 - address line 1
+=item address2 - address line 1
+=item city
+=item state
+=item zip
+=item country
+
+=item pkg - line item description
+=item setup - line item setup fee (only or both of B<setup> and B<recur> will be defined)
+=item recur - line item recurring fee (only or both of B<setup> and B<recur> will be defined)
+=item sdate - start date for recurring fee
+=item edate - end date for recurring fee
+
+=back
+
+=cut
+
+sub send_csv {
+ my($self, %opt) = @_;
+
+ #part one: create file
+
+ my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
+ mkdir $spooldir, 0700 unless -d $spooldir;
+
+ my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
+
+ open(CSV, ">$file") or die "can't open $file: $!";
+
+ eval "use Text::CSV_XS";
+ die $@ if $@;
+
+ my $csv = Text::CSV_XS->new({'always_quote'=>1});
+
+ my $cust_main = $self->cust_main;
+
+ $csv->combine(
+ 'cust_bill',
+ $self->invnum,
+ $self->custnum,
+ time2str("%x", $self->_date),
+ sprintf("%.2f", $self->charged),
+ ( map { $cust_main->getfield($_) }
+ qw( first last company address1 address2 city state zip country ) ),
+ map { '' } (1..5),
+ ) or die "can't create csv";
+ print CSV $csv->string. "\n";
+
+ #new charges (false laziness w/print_text)
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+
+ my($pkg, $setup, $recur, $sdate, $edate);
+ if ( $cust_bill_pkg->pkgnum ) {
+
+ ($pkg, $setup, $recur, $sdate, $edate) = (
+ $cust_bill_pkg->cust_pkg->part_pkg->pkg,
+ ( $cust_bill_pkg->setup != 0
+ ? sprintf("%.2f", $cust_bill_pkg->setup )
+ : '' ),
+ ( $cust_bill_pkg->recur != 0
+ ? sprintf("%.2f", $cust_bill_pkg->recur )
+ : '' ),
+ time2str("%x", $cust_bill_pkg->sdate),
+ time2str("%x", $cust_bill_pkg->edate),
+ );
+
+ } else { #pkgnum Tax
+ next unless $cust_bill_pkg->setup != 0;
+ ($pkg, $setup, $recur, $sdate, $edate) =
+ ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+ }
+
+ $csv->combine(
+ 'cust_bill_pkg',
+ $self->invnum,
+ ( map { '' } (1..11) ),
+ ($pkg, $setup, $recur, $sdate, $edate)
+ ) or die "can't create csv";
+ print CSV $csv->string. "\n";
+
+ }
+
+ close CSV or die "can't close CSV: $!";
+
+ #part two: upload it
+
+ my $net;
+ if ( $opt{protocol} eq 'ftp' ) {
+ eval "use Net::FTP;";
+ die $@ if $@;
+ $net = Net::FTP->new($opt{server}) or die @$;
+ } else {
+ die "unknown protocol: $opt{protocol}";
+ }
+
+ $net->login( $opt{username}, $opt{password} )
+ or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
+
+ $net->binary or die "can't set binary mode";
+
+ $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
+
+ $net->put($file) or die "can't put $file: $!";
+
+ $net->quit;
+
+ unlink $file;
+
+}
+
=item comp
Pays this invoice with a compliemntary payment. If there is an error,
@@ -525,12 +675,14 @@ sub realtime_card {
if ( $transaction->is_success() && $action2 ) {
my $auth = $transaction->authorization;
my $ordernum = $transaction->order_number;
+
#warn "********* $auth ***********\n";
#warn "********* $ordernum ***********\n";
my $capture =
new Business::OnlinePayment( $bop_processor, @bop_options );
- $capture->content(
+ my %capture = (
+ type => 'CC',
action => $action2,
login => $bop_login,
password => $bop_password,
@@ -538,8 +690,18 @@ sub realtime_card {
amount => $amount,
authorization => $auth,
description => $description,
+ card_number => $cust_main->payinfo,
+ expiration => $exp,
);
+ foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
+ transaction_sequence_num local_transaction_date
+ local_transaction_time AVS_result_code )) {
+ $capture{$field} = $transaction->$field() if $transaction->can($field);
+ }
+
+ $capture->content( %capture );
+
$capture->submit();
unless ( $capture->is_success ) {
@@ -590,7 +752,7 @@ sub realtime_card {
$template->compile()
or return "($perror) can't compile template: $Text::Template::ERROR";
- my $error = $transaction->error_message;
+ my $templ_hash = { error => $transaction->error_message };
#false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
$ENV{MAILADDRESS} = $invoice_from;
@@ -604,7 +766,7 @@ sub realtime_card {
] );
my $message = new Mail::Internet (
'Header' => $header,
- 'Body' => [ $template->fill_in() ],
+ 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
);
$!=0;
$message->smtpsend( Host => $smtpmachine )
@@ -726,8 +888,10 @@ sub batch_card {
'payname' => $cust_main->getfield('payname'),
'amount' => $self->owed,
} );
- $cust_pay_batch->insert;
+ my $error = $cust_pay_batch->insert;
+ die $error if $error;
+ '';
}
=item print_text [TIME];
@@ -948,7 +1112,7 @@ sub print_text {
=head1 VERSION
-$Id: cust_bill.pm,v 1.35 2002-05-18 09:51:30 ivan Exp $
+$Id: cust_bill.pm,v 1.41 2002-09-05 16:51:49 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/cust_bill_event.pm b/FS/FS/cust_bill_event.pm
index d5ca55f36..f631987aa 100644
--- a/FS/FS/cust_bill_event.pm
+++ b/FS/FS/cust_bill_event.pm
@@ -143,6 +143,21 @@ sub cust_bill {
qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
}
+=item retry
+
+Changes the status of this event from B<done> to B<failed>, allowing it to be
+retried.
+
+=cut
+
+sub retry {
+ my $self = shift;
+ return '' unless $self->status eq 'done';
+ my $old = ref($self)->new( { $self->hash } );
+ $self->status('failed');
+ $self->replace($old);
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm
index 0ce5ac614..284d59de2 100644
--- a/FS/FS/cust_credit.pm
+++ b/FS/FS/cust_credit.pm
@@ -104,8 +104,6 @@ sub insert {
return "error inserting $self: $error";
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-
#false laziness w/ cust_credit::insert
if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
my @errors = $cust_main->unsuspend;
@@ -117,6 +115,8 @@ sub insert {
}
#eslaf
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
'';
}
@@ -242,7 +242,7 @@ sub credited {
=head1 VERSION
-$Id: cust_credit.pm,v 1.15 2002-01-28 06:57:23 ivan Exp $
+$Id: cust_credit.pm,v 1.16 2002-06-04 14:35:52 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 0faa60ca6..cfa6b8bb6 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -27,6 +27,7 @@ use FS::part_pkg;
use FS::part_bill_event;
use FS::cust_bill_event;
use FS::cust_tax_exempt;
+use FS::type_pkgs;
use FS::Msgcat qw(gettext);
@ISA = qw( FS::Record );
@@ -220,7 +221,8 @@ invoicing_list destination to the newly-created svc_acct. Here's an example:
sub insert {
my $self = shift;
- my @param = @_;
+ my $cust_pkgs = @_ ? shift : {};
+ my $invoicing_list = @_ ? shift : '';
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
@@ -261,27 +263,35 @@ sub insert {
return $error;
}
- if ( @param ) { # CUST_PKG_HASHREF
- my $cust_pkgs = shift @param;
- foreach my $cust_pkg ( keys %$cust_pkgs ) {
- $cust_pkg->custnum( $self->custnum );
- $error = $cust_pkg->insert;
+ # invoicing list
+ if ( $invoicing_list ) {
+ $error = $self->check_invoicing_list( $invoicing_list );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "checking invoicing_list (transaction rolled back): $error";
+ }
+ $self->invoicing_list( $invoicing_list );
+ }
+
+ # packages
+ foreach my $cust_pkg ( keys %$cust_pkgs ) {
+ $cust_pkg->custnum( $self->custnum );
+ $error = $cust_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting cust_pkg (transaction rolled back): $error";
+ }
+ foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
+ $svc_something->pkgnum( $cust_pkg->pkgnum );
+ if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
+ $svc_something->seconds( $svc_something->seconds + $seconds );
+ $seconds = 0;
+ }
+ $error = $svc_something->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "inserting cust_pkg (transaction rolled back): $error";
- }
- foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
- $svc_something->pkgnum( $cust_pkg->pkgnum );
- if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
- $svc_something->seconds( $svc_something->seconds + $seconds );
- $seconds = 0;
- }
- $error = $svc_something->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- #return "inserting svc_ (transaction rolled back): $error";
- return $error;
- }
+ #return "inserting svc_ (transaction rolled back): $error";
+ return $error;
}
}
}
@@ -291,16 +301,6 @@ sub insert {
return "No svc_acct record to apply pre-paid time";
}
- if ( @param ) { # INVOICING_LIST_ARYREF
- my $invoicing_list = shift @param;
- $error = $self->check_invoicing_list( $invoicing_list );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "checking invoicing_list (transaction rolled back): $error";
- }
- $self->invoicing_list( $invoicing_list );
- }
-
if ( $amount ) {
my $cust_credit = new FS::cust_credit {
'custnum' => $self->custnum,
@@ -482,6 +482,32 @@ sub replace {
$self->invoicing_list( $invoicing_list );
}
+ if ( $self->payby eq 'CARD' &&
+ grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
+ # card info has changed, want to retry realtime_card invoice events
+ #false laziness w/collect
+ foreach my $cust_bill_event (
+ grep {
+ #$_->part_bill_event->plan eq 'realtime-card'
+ $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
+ && $_->status eq 'done'
+ && $_->statustext
+ }
+ map { $_->cust_bill_event }
+ grep { $_->cust_bill_event }
+ $self->open_cust_bill
+
+ ) {
+ my $error = $cust_bill_event->retry;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error scheduling invoice events for retry: $error";
+ }
+ }
+ #eslaf
+
+ }
+
#false laziness with sub insert
my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
$error = $queue->insert($self->getfield('last'), $self->company);
@@ -687,7 +713,8 @@ sub check {
my $y = length($2) == 4 ? $2 : "20$2";
$self->paydate("$y-$1-01");
my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
- return gettext('expired_card') if $y<$nowy || ( $y==$nowy && $1<$nowm );
+ return gettext('expired_card')
+ if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
}
if ( $self->payname eq '' &&
@@ -1027,8 +1054,8 @@ sub bill {
$dbh->rollback if $oldAutoCommit;
return
"fatal: can't find tax rate for state/county/country/taxclass ".
- join('/', map $self->$_(), qw(state county country taxclass) ).
- "\n";
+ join('/', ( map $self->$_(), qw(state county country) ),
+ $part_pkg->taxclass ). "\n";
};
if ( $cust_main_county->exempt_amount ) {
@@ -1171,6 +1198,8 @@ invoice_time - Use this time when deciding when to print invoices and
late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse>
for conversion functions.
+retry_card - Retry cards even when not scheduled by invoice events.
+
batch_card - This option is deprecated. See the invoice events web interface
to control whether cards are batched or run against a realtime gateway.
@@ -1203,9 +1232,29 @@ sub collect {
return '';
}
- foreach my $cust_bill (
- qsearch('cust_bill', { 'custnum' => $self->custnum, } )
- ) {
+ if ( exists($options{'retry_card'}) && $options{'retry_card'} ) {
+ #false laziness w/replace
+ foreach my $cust_bill_event (
+ grep {
+ #$_->part_bill_event->plan eq 'realtime-card'
+ $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
+ && $_->status eq 'done'
+ && $_->statustext
+ }
+ map { $_->cust_bill_event }
+ grep { $_->cust_bill_event }
+ $self->open_cust_bill
+ ) {
+ my $error = $cust_bill_event->retry;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error scheduling invoice events for retry: $error";
+ }
+ }
+ #eslaf
+ }
+
+ foreach my $cust_bill ( $self->cust_bill ) {
#this has to be before next's
my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
@@ -1223,6 +1272,7 @@ sub collect {
next unless $amount > 0;
+
foreach my $part_bill_event (
sort { $a->seconds <=> $b->seconds
|| $a->weight <=> $b->weight
@@ -1685,7 +1735,7 @@ sub credit {
$cust_credit->insert;
}
-=item charge AMOUNT PKG COMMENT
+=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
Creates a one-time charge for this customer. If there is an error, returns
the error, otherwise returns false.
@@ -1693,19 +1743,87 @@ the error, otherwise returns false.
=cut
sub charge {
- my ( $self, $amount, $pkg, $comment ) = @_;
+ my ( $self, $amount ) = ( shift, shift );
+ my $pkg = @_ ? shift : 'One-time charge';
+ my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
+ my $taxclass = @_ ? shift : '';
+
+ 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 $part_pkg = new FS::part_pkg ( {
- 'pkg' => $pkg || 'One-time charge',
- 'comment' => $comment || '$'. sprintf("%.2f".$amount),
+ 'pkg' => $pkg,
+ 'comment' => $comment,
'setup' => $amount,
'freq' => 0,
'recur' => '0',
'disabled' => 'Y',
+ 'taxclass' => $taxclass,
} );
- $part_pkg->insert;
+ my $error = $part_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ my $pkgpart = $part_pkg->pkgpart;
+ my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
+ unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
+ my $type_pkgs = new FS::type_pkgs \%type_pkgs;
+ $error = $type_pkgs->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ my $cust_pkg = new FS::cust_pkg ( {
+ 'custnum' => $self->custnum,
+ 'pkgpart' => $pkgpart,
+ } );
+ $error = $cust_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item cust_bill
+
+Returns all the invoices (see L<FS::cust_bill>) for this customer.
+
+=cut
+
+sub cust_bill {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+}
+
+=item open_cust_bill
+
+Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
+customer.
+
+=cut
+
+sub open_cust_bill {
+ my $self = shift;
+ grep { $_->owed > 0 } $self->cust_bill;
}
=back
@@ -1847,6 +1965,201 @@ sub append_fuzzyfiles {
1;
}
+=item batch_import
+
+=cut
+
+sub batch_import {
+ my $param = shift;
+ #warn join('-',keys %$param);
+ my $fh = $param->{filehandle};
+ my $agentnum = $param->{agentnum};
+ my $refnum = $param->{refnum};
+ my $pkgpart = $param->{pkgpart};
+ my @fields = @{$param->{fields}};
+
+ eval "use Date::Parse;";
+ die $@ if $@;
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+ #warn $csv;
+ #warn $fh;
+
+ my $imported = 0;
+ #my $columns;
+
+ 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;
+
+ #while ( $columns = $csv->getline($fh) ) {
+ my $line;
+ while ( defined($line=<$fh>) ) {
+
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+ #warn join('-',@columns);
+
+ my %cust_main = (
+ agentnum => $agentnum,
+ refnum => $refnum,
+ country => 'US', #default
+ payby => 'BILL', #default
+ paydate => '12/2037', #default
+ );
+ my $billtime = time;
+ my %cust_pkg = ( pkgpart => $pkgpart );
+ foreach my $field ( @fields ) {
+ if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
+ #$cust_pkg{$1} = str2time( shift @$columns );
+ if ( $1 eq 'setup' ) {
+ $billtime = str2time(shift @columns);
+ } else {
+ $cust_pkg{$1} = str2time( shift @columns );
+ }
+ } else {
+ #$cust_main{$field} = shift @$columns;
+ $cust_main{$field} = shift @columns;
+ }
+ }
+
+ my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
+ my $cust_main = new FS::cust_main ( \%cust_main );
+ use Tie::RefHash;
+ tie my %hash, 'Tie::RefHash'; #this part is important
+ $hash{$cust_pkg} = [] if $pkgpart;
+ my $error = $cust_main->insert( \%hash );
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert customer for $line: $error";
+ }
+
+ #false laziness w/bill.cgi
+ $error = $cust_main->bill( 'time' => $billtime );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't bill customer for $line: $error";
+ }
+
+ $cust_main->apply_payments;
+ $cust_main->apply_credits;
+
+ $error = $cust_main->collect();
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't collect customer for $line: $error";
+ }
+
+ $imported++;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
+=item batch_charge
+
+=cut
+
+sub batch_charge {
+ my $param = shift;
+ #warn join('-',keys %$param);
+ my $fh = $param->{filehandle};
+ my @fields = @{$param->{fields}};
+
+ eval "use Date::Parse;";
+ die $@ if $@;
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+ #warn $csv;
+ #warn $fh;
+
+ my $imported = 0;
+ #my $columns;
+
+ 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;
+
+ #while ( $columns = $csv->getline($fh) ) {
+ my $line;
+ while ( defined($line=<$fh>) ) {
+
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+ #warn join('-',@columns);
+
+ my %row = ();
+ foreach my $field ( @fields ) {
+ $row{$field} = shift @columns;
+ }
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
+ unless ( $cust_main ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "unknown custnum $row{'custnum'}";
+ }
+
+ if ( $row{'amount'} > 0 ) {
+ my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $imported++;
+ } elsif ( $row{'amount'} < 0 ) {
+ my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
+ $row{'pkg'} );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $imported++;
+ } else {
+ #hmm?
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index 28f69c262..e41564d21 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -128,6 +128,8 @@ sub regionselector {
my ( $selected_county, $selected_state, $selected_country,
$prefix, $onchange ) = @_;
+ $prefix = '' unless defined $prefix;
+
$countyflag = 0;
# unless ( @cust_main_county ) { #cache
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index fcd902b1b..98eba704b 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -152,8 +152,6 @@ sub insert {
}
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-
#false laziness w/ cust_credit::insert
if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
my @errors = $cust_main->unsuspend;
@@ -165,6 +163,8 @@ sub insert {
}
#eslaf
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
'';
}
@@ -405,7 +405,7 @@ sub unapplied {
=head1 VERSION
-$Id: cust_pay.pm,v 1.20 2002-05-18 09:51:30 ivan Exp $
+$Id: cust_pay.pm,v 1.21 2002-06-04 14:35:52 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 8b65ac4bd..12508e1aa 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -229,7 +229,7 @@ sub check {
unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
$self->otaker(getotaker) unless $self->otaker;
- $self->otaker =~ /^(\w{0,16})$/ or return "Illegal otaker";
+ $self->otaker =~ /^([\w\.\-]{0,16})$/ or return "Illegal otaker";
$self->otaker($1);
if ( $self->dbdef_table->column('manual_flag') ) {
@@ -679,7 +679,7 @@ sub order {
=head1 VERSION
-$Id: cust_pkg.pm,v 1.22 2002-05-22 12:17:06 ivan Exp $
+$Id: cust_pkg.pm,v 1.23 2002-08-26 20:40:55 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/domain_record.pm b/FS/FS/domain_record.pm
index 4ed713c77..37cc6c9e8 100644
--- a/FS/FS/domain_record.pm
+++ b/FS/FS/domain_record.pm
@@ -5,6 +5,7 @@ use vars qw( @ISA $noserial_hack );
#use FS::Record qw( qsearch qsearchs );
use FS::Record qw( qsearchs dbh );
use FS::svc_domain;
+use FS::svc_www;
@ISA = qw(FS::Record);
@@ -124,6 +125,9 @@ Delete this record from the database.
sub delete {
my $self = shift;
+ return "Can't delete a domain record which has a website!"
+ if qsearchs( 'svc_www', { 'recnum' => $self->recnum } );
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
@@ -296,7 +300,7 @@ sub increment_serial {
=item svc_domain
-Returns the domain (see L<FS::svc_domain) for this record.
+Returns the domain (see L<FS::svc_domain>) for this record.
=cut
@@ -309,7 +313,7 @@ sub svc_domain {
=head1 VERSION
-$Id: domain_record.pm,v 1.9 2002-05-23 13:00:08 ivan Exp $
+$Id: domain_record.pm,v 1.11 2002-06-23 19:16:45 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
index 08e436da0..3c1a3de8e 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -370,7 +370,8 @@ sub rebless {
my $self = shift;
my $exporttype = $self->exporttype;
my $class = ref($self). "::$exporttype";
- eval "use $class;" or die $@;
+ eval "use $class;";
+ die $@ if $@;
bless($self, $class);
}
@@ -413,6 +414,26 @@ sub export_delete {
$self->_export_delete(@_);
}
+=item export_suspend
+
+=cut
+
+sub export_suspend {
+ my $self = shift;
+ $self->rebless;
+ $self->_export_suspend(@_);
+}
+
+=item export_unsuspend
+
+=cut
+
+sub export_unsuspend {
+ my $self = shift;
+ $self->rebless;
+ $self->_export_unsuspend(@_);
+}
+
#fallbacks providing useful error messages intead of infinite loops
sub _export_insert {
my $self = shift;
@@ -429,6 +450,20 @@ sub _export_delete {
return "_export_delete: unknown export type ". $self->exporttype;
}
+#fallbacks providing null operations
+
+sub _export_suspend {
+ my $self = shift;
+ #warn "warning: _export_suspened unimplemented for". ref($self);
+ '';
+}
+
+sub _export_unsuspend {
+ my $self = shift;
+ #warn "warning: _export_unsuspend unimplemented for ". ref($self);
+ '';
+}
+
=back
=head1 SUBROUTINES
@@ -459,33 +494,55 @@ sub export_info {
my $r = { map { %{$exports{$_}} } keys %exports };
}
-=item exporttype2svcdb EXPORTTYPE
-
-Returns the applicable I<svcdb> for an I<exporttype>.
+#=item exporttype2svcdb EXPORTTYPE
+#
+#Returns the applicable I<svcdb> for an I<exporttype>.
+#
+#=cut
+#
+#sub exporttype2svcdb {
+# my $exporttype = $_[0];
+# foreach my $svcdb ( keys %exports ) {
+# return $svcdb if grep { $exporttype eq $_ } keys %{$exports{$svcdb}};
+# }
+# '';
+#}
-=cut
+tie my %sysvshell_options, 'Tie::IxHash',
+ 'crypt' => { label=>'Password encryption',
+ type=>'select', options=>[qw(crypt md5)],
+ default=>'crypt',
+ },
+;
-sub exporttype2svcdb {
- my $exporttype = $_[0];
- foreach my $svcdb ( keys %exports ) {
- return $svcdb if grep { $exporttype eq $_ } keys %{$exports{$svcdb}};
- }
- '';
-}
+tie my %bsdshell_options, 'Tie::IxHash',
+ 'crypt' => { label=>'Password encryption',
+ type=>'select', options=>[qw(crypt md5)],
+ default=>'crypt',
+ },
+;
tie my %shellcommands_options, 'Tie::IxHash',
#'machine' => { label=>'Remote machine' },
'user' => { label=>'Remote username', default=>'root' },
'useradd' => { label=>'Insert command',
- default=>'useradd -d $dir -m -s $shell -u $uid $username'
+ default=>'useradd -d $dir -m -s $shell -u $uid -p $crypt_password $username'
#default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
},
+ 'useradd_stdin' => { label=>'Insert command STDIN',
+ type =>'textarea',
+ default=>'',
+ },
'userdel' => { label=>'Delete command',
- default=>'userdel $username',
+ default=>'userdel -r $username',
#default=>'rm -rf $dir',
},
+ 'userdel_stdin' => { label=>'Delete command STDIN',
+ type =>'textarea',
+ default=>'',
+ },
'usermod' => { label=>'Modify command',
- default=>'usermod -d $new_dir -l $new_username -s $new_shell -u $new_uid $old_username',
+ default=>'usermod -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username',
#default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
# 'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
# 'find . -depth -print | cpio -pdm $new_dir; '.
@@ -493,6 +550,66 @@ tie my %shellcommands_options, 'Tie::IxHash',
# 'rm -rf $old_dir'.
#')'
},
+ 'usermod_stdin' => { label=>'Modify command STDIN',
+ type =>'textarea',
+ default=>'',
+ },
+;
+
+tie my %shellcommands_withdomain_options, 'Tie::IxHash',
+ 'user' => { label=>'Remote username', default=>'root' },
+ 'useradd' => { label=>'Insert command',
+ #default=>''
+ },
+ 'useradd_stdin' => { label=>'Insert command STDIN',
+ type =>'textarea',
+ #default=>"$_password\n$_password\n",
+ },
+ 'userdel' => { label=>'Delete command',
+ #default=>'',
+ },
+ 'userdel_stdin' => { label=>'Delete command STDIN',
+ type =>'textarea',
+ #default=>'',
+ },
+ 'usermod' => { label=>'Modify command',
+ default=>'',
+ },
+ 'usermod_stdin' => { label=>'Modify command STDIN',
+ type =>'textarea',
+ #default=>"$_password\n$_password\n",
+ },
+;
+
+tie my %www_shellcommands_options, 'Tie::IxHash',
+ 'user' => { lable=>'Remote username', default=>'root' },
+ 'useradd' => { label=>'Insert command',
+ default=>'mkdir /var/www/$zone; chown $username /var/www/$zone; ln -s /var/www/$zone $homedir/$zone',
+ },
+ 'userdel' => { label=>'Delete command',
+ default=>'[ -n &quot;$zone&quot; ] && rm -rf /var/www/$zone; rm $homedir/$zone',
+ },
+ 'usermod' => { label=>'Modify command',
+ default=>'[ -n &quot;$old_zone&quot; ] && rm $old_homedir/$old_zone; [ &quot;$old_zone&quot; != &quot;$new_zone&quot; -a -n &quot;$new_zone&quot; ] && mv /var/www/$old_zone /var/www/$new_zone; [ &quot;$old_username&quot; != &quot;$new_username&quot; ] && chown -R $new_username /var/www/$new_zone; ln -s /var/www/$new_zone $new_homedir/$new_zone',
+ },
+;
+
+tie my %domain_shellcommands_options, 'Tie::IxHash',
+ 'user' => { lable=>'Remote username', default=>'root' },
+ 'useradd' => { label=>'Insert command',
+ default=>'',
+ },
+ 'userdel' => { label=>'Delete command',
+ default=>'',
+ },
+ 'usermod' => { label=>'Modify command',
+ default=>'',
+ },
+;
+
+tie my %textradius_options, 'Tie::IxHash',
+ 'user' => { label=>'Remote username', default=>'root' },
+ 'users' => { label=>'users file location', default=>'/etc/raddb/users' },
;
tie my %sqlradius_options, 'Tie::IxHash',
@@ -524,7 +641,7 @@ tie my %infostreet_options, 'Tie::IxHash',
;
tie my %vpopmail_options, 'Tie::IxHash',
- 'machine' => { label=>'vpopmail machine', },
+ #'machine' => { label=>'vpopmail machine', },
'dir' => { label=>'directory', }, # ?more info? default?
'uid' => { label=>'vpopmail uid' },
'gid' => { label=>'vpopmail gid' },
@@ -545,6 +662,42 @@ tie my %bind_slave_options, 'Tie::IxHash',
default => '/etc/bind/named.conf' },
;
+tie my %http_options, 'Tie::IxHash',
+ 'method' => { label =>'Method',
+ type =>'select',
+ #options =>[qw(POST GET)],
+ options =>[qw(POST)],
+ default =>'POST' },
+ 'url' => { label => 'URL', default => 'http://', },
+ 'insert_data' => {
+ label => 'Insert data',
+ type => 'textarea',
+ default => join("\n",
+ 'DomainName $svc_x->domain',
+ 'Email ( grep { $_ ne "POST" } $svc_x->cust_svc->cust_pkg->cust_main->invoicing_list)[0]',
+ 'test 1',
+ 'reseller $svc_x->cust_svc->cust_pkg->part_pkg->pkg =~ /reseller/i',
+ ),
+ },
+ 'delete_data' => {
+ label => 'Delete data',
+ type => 'textarea',
+ default => join("\n",
+ ),
+ },
+ 'replace_data' => {
+ label => 'Replace data',
+ type => 'textarea',
+ default => join("\n",
+ ),
+ },
+;
+
+tie my %sqlmail_options, 'Tie::IxHash',
+ 'datasrc' => { label=>'DBI data source' },
+ 'username' => { label=>'Database username' },
+ 'password' => { label=>'Database password' },
+;
#export names cannot have dashes...
@@ -552,13 +705,17 @@ tie my %bind_slave_options, 'Tie::IxHash',
'svc_acct' => {
'sysvshell' => {
'desc' =>
- 'Batch export of /etc/passwd and /etc/shadow files (Linux/SysV)',
- 'options' => {},
+ 'Batch export of /etc/passwd and /etc/shadow files (Linux/SysV).',
+ 'options' => \%sysvshell_options,
+ 'nodomain' => 'Y',
+ 'notes' => 'MD5 crypt requires installation of <a href="http://search.cpan.org/search?dist=Crypt-PasswdMD5">Crypt::PasswdMD5</a> from CPAN. Run bin/sysvshell.export to export the files.',
},
'bsdshell' => {
'desc' =>
- 'Batch export of /etc/passwd and /etc/master.passwd files (BSD)',
- 'options' => {},
+ 'Batch export of /etc/passwd and /etc/master.passwd files (BSD).',
+ 'options' => \%bsdshell_options,
+ 'nodomain' => 'Y',
+ 'notes' => 'MD5 crypt requires installation of <a href="http://search.cpan.org/search?dist=Crypt-PasswdMD5">Crypt::PasswdMD5</a> from CPAN. Run bin/bsdshell.export to export the files.',
},
# 'nis' => {
# 'desc' =>
@@ -566,22 +723,36 @@ tie my %bind_slave_options, 'Tie::IxHash',
# 'options' => {},
# },
'textradius' => {
- 'desc' => 'Batch export of a text /etc/raddb/users file (Livingston, Cistron)',
- 'options' => {},
+ 'desc' => 'Real-time export to a text /etc/raddb/users file (Livingston, Cistron)',
+ 'options' => \%textradius_options,
+ 'notes' => 'This will edit a text RADIUS users file in place on a remote server. Requires installation of <a href="http://search.cpan.org/search?dist=RADIUS-UserFile">RADIUS::UserFile</a> from CPAN. If using RADIUS::UserFile 1.01, make sure to apply <a href="http://rt.cpan.org/NoAuth/Bug.html?id=1210">this patch</a>. Also make sure <a href="http://rsync.samba.org/">rsync</a> is installed on the remote machine, and <a href="../docs/ssh.html">SSH is setup for unattended operation</a>.',
},
'shellcommands' => {
'desc' => 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
'options' => \%shellcommands_options,
'nodomain' => 'Y',
- 'notes' => 'shellcommandsnotes... (this one is the nodomain one)',
+ 'notes' => 'Run remote commands via SSH. Usernames are considered unique (also see shellcommands_withdomain). You probably want this if the commands you are running will not accept a domain as a parameter. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="Linux/NetBSD" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = "";\'><LI><INPUT TYPE="button" VALUE="FreeBSD" onClick=\'this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0"; this.form.useradd_stdin.value = "$_password\n"; this.form.userdel.value = "pw userdel $username -r"; this.form.userdel_stdin.value=""; this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0"; this.form.usermod_stdin.value = "$new__password\n";\'><LI><INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick=\'this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = ""; this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $uid.$gid $new_dir; rm -rf $old_dir )"; this.form.usermod_stdin.value = ""; this.form.userdel.value = "rm -rf $dir"; this.form.userdel_stdin.value="";\'></UL>',
+ },
+
+ 'shellcommands_withdomain' => {
+ 'desc' => 'Real-time export via remote SSH.',
+ 'options' => \%shellcommands_withdomain_options,
+ 'notes' => 'Run remote commands via SSH. username@domain (rather than just usernames) are considered unique (also see shellcommands). You probably want this if the commands you are running will accept a domain as a parameter, and will allow the same username with different domains. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.',
},
'sqlradius' => {
'desc' => 'Real-time export to SQL-backed RADIUS (ICRADIUS, FreeRADIUS)',
'options' => \%sqlradius_options,
'nodomain' => 'Y',
- 'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>. Use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete and repopulate the tables from the Freeside database. See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.23/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source. If using <a href="http://www.freeradius.org/">FreeRADIUS</a> 0.5 or above, make sure your <b>op</b> fields are set to allow NULL values.',
+ 'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>. An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database. See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.23/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source. If using <a href="http://www.freeradius.org/">FreeRADIUS</a> 0.5 or above, make sure your <b>op</b> fields are set to allow NULL values.',
+ },
+
+ 'sqlmail' => {
+ 'desc' => 'Real-time export to SQL-backed mail server',
+ 'options' => \%sqlmail_options,
+ 'nodomain' => 'Y',
+ 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from pc-intouch?...)',
},
'cyrus' => {
@@ -607,8 +778,7 @@ tie my %bind_slave_options, 'Tie::IxHash',
'vpopmail' => {
'desc' => 'Real-time export to vpopmail text files',
'options' => \%vpopmail_options,
-
- 'notes' => 'Real time export to <a href="http://inter7.com/vpopmail/">vpopmail</a> text files (...extended description from jeff?...)',
+ 'notes' => 'Real time export to <a href="http://inter7.com/vpopmail/">vpopmail</a> text files. <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed, and you will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>',
},
},
@@ -618,13 +788,32 @@ tie my %bind_slave_options, 'Tie::IxHash',
'bind' => {
'desc' =>'Batch export to BIND named',
'options' => \%bind_options,
- 'notes' => 'bind export notes File::Rsync dependancy, run bind.export',
+ 'notes' => 'Batch export of BIND zone and configuration files to primary nameserver. <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed. Run bin/bind.export to export the files.',
},
'bind_slave' => {
'desc' =>'Batch export to slave BIND named',
'options' => \%bind_slave_options,
- 'notes' => 'bind export notes (secondary munge) File::Rsync dependancy, run bind.export',
+ 'notes' => 'Batch export of BIND configuration file to a secondary nameserver. Zones are slaved from the listed masters. <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed. Run bin/bind.export to export the files.',
+ },
+
+ 'http' => {
+ 'desc' => 'Send an HTTP or HTTPS GET or POST request',
+ 'options' => \%http_options,
+ 'notes' => 'Send an HTTP or HTTPS GET or POST to the specified URL. <a href="http://search.cpan.org/search?dist=libwww-perl">libwww-perl</a> must be installed. For HTTPS support, <a href="http://search.cpan.org/search?dist=Crypt-SSLeay">Crypt::SSLeay</a> or <a href="http://search.cpan.org/search?dist=IO-Socket-SSL">IO::Socket::SSL</a> is required.',
+ },
+
+ 'sqlmail' => {
+ 'desc' => 'Real-time export to SQL-backed mail server',
+ 'options' => \%sqlmail_options,
+ #'nodomain' => 'Y',
+ 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from pc-intouch?...)',
+ },
+
+ 'domain_shellcommands' => {
+ 'desc' => 'Run remote commands via SSH, for domains.',
+ 'options' => \%domain_shellcommands_options,
+ 'notes' => 'Run remote commands via SSH, for domains. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.',
},
@@ -632,9 +821,23 @@ tie my %bind_slave_options, 'Tie::IxHash',
'svc_acct_sm' => {},
- 'svc_forward' => {},
+ 'svc_forward' => {
+ 'sqlmail' => {
+ 'desc' => 'Real-time export to SQL-backed mail server',
+ 'options' => \%sqlmail_options,
+ #'nodomain' => 'Y',
+ 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from pc-intouch?...)',
+ },
+ },
- 'svc_www' => {},
+ 'svc_www' => {
+ 'www_shellcommands' => {
+ 'desc' => 'Run remote commands via SSH, for virtual web sites.',
+ 'options' => \%www_shellcommands_options,
+ 'notes' => 'Run remote commands via SSH, for virtual web sites. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.',
+ },
+
+ },
);
@@ -647,7 +850,8 @@ FS/FS/part_export/ (an example may be found in eg/export_template.pm)
=head1 BUGS
-Probably.
+All the stuff in the %exports hash should be generated from the specific
+export modules.
Hmm... cust_export class (not necessarily a database table...) ... ?
diff --git a/FS/FS/part_export/bind.pm b/FS/FS/part_export/bind.pm
new file mode 100644
index 000000000..b72c9bdb0
--- /dev/null
+++ b/FS/FS/part_export/bind.pm
@@ -0,0 +1,7 @@
+package FS::part_export::bind;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/bind_slave.pm b/FS/FS/part_export/bind_slave.pm
new file mode 100644
index 000000000..ebb29c1d7
--- /dev/null
+++ b/FS/FS/part_export/bind_slave.pm
@@ -0,0 +1,7 @@
+package FS::part_export::bind_slave;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/bsdshell.pm b/FS/FS/part_export/bsdshell.pm
index 4a890d051..06642097f 100644
--- a/FS/FS/part_export/bsdshell.pm
+++ b/FS/FS/part_export/bsdshell.pm
@@ -1,50 +1,7 @@
package FS::part_export::bsdshell;
use vars qw(@ISA);
-use FS::part_export;
+use FS::part_export::null;
-@ISA = qw(FS::part_export);
-
-sub rebless { shift; }
-
-sub _export_insert {
- my($self, $svc_acct) = (shift, shift);
- $err_or_queue = $self->bsdshell_queue( $svc_acct->svcnum, 'insert',
- $svc_acct->username, $svc_acct->_password );
- ref($err_or_queue) ? '' : $err_or_queue;
-}
-
-sub _export_replace {
- my( $self, $new, $old ) = (shift, shift, shift);
- #return "can't change username with bsdshell"
- # if $old->username ne $new->username;
- #return '' unless $old->_password ne $new->_password;
- $err_or_queue = $self->bsdshell_queue( $new->svcnum,
- 'replace', $new->username, $new->_password );
- ref($err_or_queue) ? '' : $err_or_queue;
-}
-
-sub _export_delete {
- my( $self, $svc_acct ) = (shift, shift);
- $err_or_queue = $self->bsdshell_queue( $svc_acct->svcnum,
- 'delete', $svc_acct->username );
- ref($err_or_queue) ? '' : $err_or_queue;
-}
-
-#a good idea to queue anything that could fail or take any time
-sub bsdshell_queue {
- my( $self, $svcnum, $method ) = (shift, shift, shift);
- my $queue = new FS::queue {
- 'svcnum' => $svcnum,
- 'job' => "FS::part_export::bsdshell::bsdshell_$method",
- };
- $queue->insert( @_ ) or $queue;
-}
-
-sub bsdshell_insert { #subroutine, not method
-}
-sub bsdshell_replace { #subroutine, not method
-}
-sub bsdshell_delete { #subroutine, not method
-}
+@ISA = qw(FS::part_export::null);
diff --git a/FS/FS/part_export/domain_shellcommands.pm b/FS/FS/part_export/domain_shellcommands.pm
new file mode 100644
index 000000000..5b3cd5d79
--- /dev/null
+++ b/FS/FS/part_export/domain_shellcommands.pm
@@ -0,0 +1,115 @@
+package FS::part_export::domain_shellcommands;
+
+use strict;
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+ my($self) = shift;
+ $self->_export_command('useradd', @_);
+}
+
+sub _export_delete {
+ my($self) = shift;
+ $self->_export_command('userdel', @_);
+}
+
+sub _export_command {
+ my ( $self, $action, $svc_domain) = (shift, shift, shift);
+ my $command = $self->option($action);
+
+ #set variable for the command
+ no strict 'vars';
+ {
+ no strict 'refs';
+ ${$_} = $svc_domain->getfield($_) foreach $svc_domain->fields;
+ }
+
+# my $domain_record = $svc_www->domain_record; # or die ?
+# my $zone = $domain_record->reczone; # or die ?
+# unless ( $zone =~ /\.$/ ) {
+# my $svc_domain = $domain_record->svc_domain; # or die ?
+# $zone .= '.'. $svc_domain->domain;
+# }
+
+# my $svc_acct = $svc_www->svc_acct; # or die ?
+# my $username = $svc_acct->username;
+# my $homedir = $svc_acct->dir; # or die ?
+
+ #done setting variables for the command
+
+ $self->shellcommands_queue( $svc_domain->svcnum,
+ user => $self->option('user')||'root',
+ host => $self->machine,
+ command => eval(qq("$command")),
+ );
+}
+
+sub _export_replace {
+ my($self, $new, $old ) = (shift, shift, shift);
+ my $command = $self->option('usermod');
+
+ #set variable for the command
+ no strict 'vars';
+ {
+ no strict 'refs';
+ ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+ ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+ }
+# my $old_domain_record = $old->domain_record; # or die ?
+# my $old_zone = $old_domain_record->reczone; # or die ?
+# unless ( $old_zone =~ /\.$/ ) {
+# my $old_svc_domain = $old_domain_record->svc_domain; # or die ?
+# $old_zone .= '.'. $old_svc_domain->domain;
+# }
+#
+# my $old_svc_acct = $old->svc_acct; # or die ?
+# my $old_username = $old_svc_acct->username;
+# my $old_homedir = $old_svc_acct->dir; # or die ?
+#
+# my $new_domain_record = $new->domain_record; # or die ?
+# my $new_zone = $new_domain_record->reczone; # or die ?
+# unless ( $new_zone =~ /\.$/ ) {
+# my $new_svc_domain = $new_domain_record->svc_domain; # or die ?
+# $new_zone .= '.'. $new_svc_domain->domain;
+# }
+
+# my $new_svc_acct = $new->svc_acct; # or die ?
+# my $new_username = $new_svc_acct->username;
+# my $new_homedir = $new_svc_acct->dir; # or die ?
+
+ #done setting variables for the command
+
+ $self->shellcommands_queue( $new->svcnum,
+ user => $self->option('user')||'root',
+ host => $self->machine,
+ command => eval(qq("$command")),
+ );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub shellcommands_queue {
+ my( $self, $svcnum ) = (shift, shift);
+ my $queue = new FS::queue {
+ 'svcnum' => $svcnum,
+ 'job' => "FS::part_export::domain_shellcommands::ssh_cmd",
+ };
+ $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+ use Net::SSH '0.07';
+ &Net::SSH::ssh_cmd( { @_ } );
+}
+
+#sub shellcommands_insert { #subroutine, not method
+#}
+#sub shellcommands_replace { #subroutine, not method
+#}
+#sub shellcommands_delete { #subroutine, not method
+#}
+
diff --git a/FS/FS/part_export/http.pm b/FS/FS/part_export/http.pm
new file mode 100644
index 000000000..0e02f0f8e
--- /dev/null
+++ b/FS/FS/part_export/http.pm
@@ -0,0 +1,88 @@
+package FS::part_export::http;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+ my $self = shift;
+ $self->_export_command('insert', @_);
+}
+
+sub _export_delete {
+ my $self = shift;
+ $self->_export_command('delete', @_);
+}
+
+sub _export_command {
+ my( $self, $action, $svc_x ) = ( shift, shift, shift );
+
+ return unless $self->option("${action}_data");
+
+ $self->http_queue( $svc_x->svcnum,
+ $self->option('method'),
+ $self->option('url'),
+ map {
+ /^\s*(\S+)\s+(.*)$/ or /()()/;
+ my( $field, $value_expression ) = ( $1, $2 );
+ my $value = eval $value_expression;
+ die $@ if $@;
+ ( $field, $value );
+ } split(/\n/, $self->option("${action}_data") )
+ );
+
+}
+
+sub _export_replace {
+ my( $self, $new, $old ) = ( shift, shift, shift );
+
+ return unless $self->option('replace_data');
+
+ $self->http_queue( $svc_x->svcnum,
+ $self->option('method'),
+ $self->option('url'),
+ map {
+ /^\s*(\S+)\s+(.*)$/ or /()()/;
+ my( $field, $value_expression ) = ( $1, $2 );
+ die $@ if $@;
+ ( $field, $value );
+ } split(/\n/, $self->option('replace_data') )
+ );
+
+}
+
+sub http_queue {
+ my($self, $svcnum) = (shift, shift);
+ my $queue = new FS::queue {
+ 'svcnum' => $svcnum,
+ 'job' => "FS::part_export::http::http",
+ };
+ $queue->insert( @_ );
+}
+
+sub http {
+ my($method, $url, @data) = @_;
+
+ $method = lc($method);
+
+ eval "use LWP::UserAgent;";
+ die "using LWP::UserAgent: $@" if $@;
+ eval "use HTTP::Request::Common;";
+ die "using HTTP::Request::Common: $@" if $@;
+
+ my $ua = LWP::UserAgent->new;
+
+ #my $response = $ua->$method(
+ # $url, \%data,
+ # 'Content-Type'=>'application/x-www-form-urlencoded'
+ #);
+ my $req = HTTP::Request::Common::POST( $url, \@data );
+ my $response = $ua->request($req);
+
+ die $response->error_as_HTML if $response->is_error;
+
+}
+
diff --git a/FS/FS/part_export/infostreet.pm b/FS/FS/part_export/infostreet.pm
index 2ce556339..f2d519932 100644
--- a/FS/FS/part_export/infostreet.pm
+++ b/FS/FS/part_export/infostreet.pm
@@ -1,16 +1,67 @@
package FS::part_export::infostreet;
-use vars qw(@ISA);
+use vars qw(@ISA %infostreet2cust_main $DEBUG);
+use FS::UID qw(dbh);
use FS::part_export;
@ISA = qw(FS::part_export);
+$DEBUG = 0;
+
+%infostreet2cust_main = (
+ 'firstName' => 'first',
+ 'lastName' => 'last',
+ 'address1' => 'address1',
+ 'address2' => 'address2',
+ 'city' => 'city',
+ 'state' => 'state',
+ 'zipCode' => 'zip',
+ 'country' => 'country',
+ 'phoneNumber' => 'daytime',
+ 'faxNumber' => 'night', #noment-request...
+);
+
sub rebless { shift; }
sub _export_insert {
my( $self, $svc_acct ) = (shift, shift);
- $self->infostreet_queue( $svc_acct->svcnum,
+ my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main;
+
+ 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 $err_or_queue = $self->infostreet_err_or_queue( $svc_acct->svcnum,
'createUser', $svc_acct->username, $svc_acct->_password );
+ return $err_or_queue unless ref($err_or_queue);
+ my $jobnum = $err_or_queue->jobnum;
+
+ my %contact_info = ( map {
+ $_ => $cust_main->getfield( $infostreet2cust_main{$_} );
+ } keys %infostreet2cust_main );
+
+ my @emails = grep { $_ ne 'POST' } $cust_main->invoicing_list;
+ $contact_info{'email'} = $emails[0] if @emails;
+
+ #this one is kinda noment-specific
+ $contact_info{'organization'} = $cust_main->agent->agent;
+
+ $err_or_queue = $self->infostreet_queueContact( $svc_acct->svcnum,
+ $svc_acct->username, %contact_info );
+ return $err_or_queue unless ref($err_or_queue);
+ my $error = $err_or_queue->depend_insert( $jobnum );
+ return $error if $error;
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
}
sub _export_replace {
@@ -28,6 +79,18 @@ sub _export_delete {
'purgeAccount,releaseUsername', $svc_acct->username );
}
+sub _export_suspend {
+ my( $self, $svc_acct ) = (shift, shift);
+ $self->infostreet_queue( $svc_acct->svcnum,
+ 'setStatus', $svc_acct->username, 'DISABLED' );
+}
+
+sub _export_unsuspend {
+ my( $self, $svc_acct ) = (shift, shift);
+ $self->infostreet_queue( $svc_acct->svcnum,
+ 'setStatus', $svc_acct->username, 'ACTIVE' );
+}
+
sub infostreet_queue {
my( $self, $svcnum, $method ) = (shift, shift, shift);
my $queue = new FS::queue {
@@ -44,9 +107,54 @@ sub infostreet_queue {
);
}
+#ick false laziness
+sub infostreet_err_or_queue {
+ my( $self, $svcnum, $method ) = (shift, shift, shift);
+ my $queue = new FS::queue {
+ 'svcnum' => $svcnum,
+ 'job' => 'FS::part_export::infostreet::infostreet_command',
+ };
+ $queue->insert(
+ $self->option('url'),
+ $self->option('login'),
+ $self->option('password'),
+ $self->option('groupID'),
+ $method,
+ @_,
+ ) or $queue;
+}
+
+sub infostreet_queueContact {
+ my( $self, $svcnum ) = (shift, shift);
+ my $queue = new FS::queue {
+ 'svcnum' => $svcnum,
+ 'job' => 'FS::part_export::infostreet::infostreet_setContact',
+ };
+ $queue->insert(
+ $self->option('url'),
+ $self->option('login'),
+ $self->option('password'),
+ $self->option('groupID'),
+ @_,
+ ) or $queue;
+}
+
+sub infostreet_setContact {
+ my($url, $is_username, $is_password, $groupID, $username, %contact_info) = @_;
+ my $accountID = infostreet_command($url, $is_username, $is_password, $groupID,
+ 'getAccountID', $username);
+ foreach my $field ( keys %contact_info ) {
+ infostreet_command($url, $is_username, $is_password, $groupID,
+ 'setContactField', [ 'int'=>$accountID ], $field, $contact_info{$field} );
+ }
+
+}
+
sub infostreet_command { #subroutine, not method
my($url, $username, $password, $groupID, $method, @args) = @_;
+ warn "[FS::part_export::infostreet] $method ".join(' ', @args)."\n" if $DEBUG;
+
#quelle hack
if ( $method =~ /,/ ) {
foreach my $part ( split(/,\s*/, $method) ) {
@@ -56,6 +164,15 @@ sub infostreet_command { #subroutine, not method
}
eval "use Frontier::Client;";
+ die $@ if $@;
+
+ eval 'sub Frontier::RPC2::String::repr {
+ my $self = shift;
+ my $value = $$self;
+ $value =~ s/([&<>\"])/$Frontier::RPC2::char_entities{$1}/ge;
+ $value;
+ }';
+ die $@ if $@;
my $conn = Frontier::Client->new( url => $url );
my $key_result = $conn->call( 'authenticate', $username, $password, $groupID);
@@ -64,12 +181,29 @@ sub infostreet_command { #subroutine, not method
my $key = $key_result{data};
#my $result = $conn->call($method, $key, @args);
- my $result = $conn->call($method, $key, map { $conn->string($_) } @args);
+ my $result = $conn->call( $method, $key,
+ map {
+ if ( ref($_) ) {
+ my( $type, $value) = @{$_};
+ $conn->$type($value);
+ } else {
+ $conn->string($_);
+ }
+ } @args );
my %result = _infostreet_parse($result);
die $result{error} unless $result{success};
+ $result->{data};
+
}
+#sub infostreet_command_byid { #subroutine, not method;
+# my($url, $username, $password, $groupID, $method, @args ) = @_;
+#
+# infostreet_command
+#
+#}
+
sub _infostreet_parse { #subroutine, not method
my $arg = shift;
map {
diff --git a/FS/FS/part_export/null.pm b/FS/FS/part_export/null.pm
new file mode 100644
index 000000000..0145af3a4
--- /dev/null
+++ b/FS/FS/part_export/null.pm
@@ -0,0 +1,13 @@
+package FS::part_export::null;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {}
+sub _export_replace {}
+sub _export_delete {}
+
diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm
index 7a87bd3e0..a514f9375 100644
--- a/FS/FS/part_export/shellcommands.pm
+++ b/FS/FS/part_export/shellcommands.pm
@@ -1,10 +1,13 @@
package FS::part_export::shellcommands;
-use vars qw(@ISA);
+use vars qw(@ISA @saltset);
+use String::ShellQuote;
use FS::part_export;
@ISA = qw(FS::part_export);
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
sub rebless { shift; }
sub _export_insert {
@@ -20,23 +23,47 @@ sub _export_delete {
sub _export_command {
my ( $self, $action, $svc_acct) = (shift, shift, shift);
my $command = $self->option($action);
- no strict 'refs';
- ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
+ my $stdin = $self->option($action."_stdin");
+ no strict 'vars';
+ {
+ no strict 'refs';
+ ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
+ }
+ $finger = shell_quote $finger;
+ $quoted_password = shell_quote $_password;
+ $domain = $svc_acct->domain;
+ $crypt_password = ''; #surpress "used only once" warnings
+ $crypt_password = crypt( $svc_acct->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))] );
$self->shellcommands_queue( $svc_acct->svcnum,
- $self->options('user')||'root'. "\@". $self->options('machine'),
- eval(qq("$command"))
+ user => $self->option('user')||'root',
+ host => $self->machine,
+ command => eval(qq("$command")),
+ stdin_string => eval(qq("$stdin")),
);
}
sub _export_replace {
my($self, $new, $old ) = (shift, shift, shift);
my $command = $self->option('usermod');
- no strict 'refs';
- ${"old_$_"} = $old->getfield($_) foreach $old->fields;
- ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+ my $stdin = $self->option('usermod_stdin');
+ no strict 'vars';
+ {
+ no strict 'refs';
+ ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+ ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+ }
+ $new_finger = shell_quote $new_finger;
+ $quoted_new__password = shell_quote $new__password;
+ $new_domain = $new->domain;
+ $new_crypt_password = ''; #surpress "used only once" warnings
+ $new_crypt_password = crypt( $new->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))]);
$self->shellcommands_queue( $new->svcnum,
- $self->options('user')||'root'. "\@". $self->options('machine'),
- eval(qq("$command"))
+ user => $self->option('user')||'root',
+ host => $self->machine,
+ command => eval(qq("$command")),
+ stdin_string => eval(qq("$stdin")),
);
}
@@ -45,11 +72,16 @@ sub shellcommands_queue {
my( $self, $svcnum ) = (shift, shift);
my $queue = new FS::queue {
'svcnum' => $svcnum,
- 'job' => "Net::SSH::ssh_cmd", #freeside-queued pre-uses...
+ 'job' => "FS::part_export::shellcommands::ssh_cmd",
};
$queue->insert( @_ );
}
+sub ssh_cmd { #subroutine, not method
+ use Net::SSH '0.07';
+ &Net::SSH::ssh_cmd( { @_ } );
+}
+
#sub shellcommands_insert { #subroutine, not method
#}
#sub shellcommands_replace { #subroutine, not method
diff --git a/FS/FS/part_export/shellcommands_withdomain.pm b/FS/FS/part_export/shellcommands_withdomain.pm
new file mode 100644
index 000000000..a15c24d88
--- /dev/null
+++ b/FS/FS/part_export/shellcommands_withdomain.pm
@@ -0,0 +1,7 @@
+package FS::part_export::shellcommands_withdomain;
+
+use vars qw(@ISA);
+use FS::part_export::shellcommands;
+
+@ISA = qw(FS::part_export::shellcommands);
+
diff --git a/FS/FS/part_export/sqlmail.pm b/FS/FS/part_export/sqlmail.pm
new file mode 100644
index 000000000..4194daf0c
--- /dev/null
+++ b/FS/FS/part_export/sqlmail.pm
@@ -0,0 +1,111 @@
+package FS::part_export::sqlmail;
+
+use vars qw(@ISA %fs_mail_table %fields);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+%fs_mail_table = ( svc_acct => 'user',
+ svc_domain => 'domain' );
+
+# fields that need to be copied into the fs_mail tables
+$fields{user} = [qw(username _password finger domsvc svcnum )];
+$fields{domain} = [qw(domain svcnum catchall )];
+
+sub rebless { shift; }
+
+sub _export_insert {
+ my($self, $svc) = (shift, shift);
+ # this is a svc_something.
+
+ my $table = $fs_mail_table{$svc->cust_svc->part_svc->svcdb};
+ my @attrib = map {$svc->$_} @{$fields{$table}};
+ my $error = $self->sqlmail_queue( $svc->svcnum, 'insert',
+ $table, @attrib );
+ return $error if $error;
+ '';
+}
+
+sub _export_replace {
+ my( $self, $new, $old ) = (shift, shift, shift);
+
+ my $table = $fs_mail_table{$new->cust_svc->part_svc->svcdb};
+
+ my @old = ($old->svcnum, 'delete', $table, $old->svcnum);
+ my @narf = map {$new->$_} @{$fields{$table}};
+ $self->sqlmail_queue($new->svcnum, 'replace', $table,
+ $new->svcnum, @narf);
+
+ return $error if $error;
+ '';
+}
+
+sub _export_delete {
+ my( $self, $svc ) = (shift, shift);
+ my $table = $fs_mail_table{$new->cust_svc->part_svc->svcdb};
+ $self->sqlmail_queue( $svc->svcnum, 'delete', $table,
+ $svc->svcnum );
+}
+
+sub sqlmail_queue {
+ my( $self, $svcnum, $method, $table ) = (shift, shift, shift);
+ my $queue = new FS::queue {
+ 'svcnum' => $svcnum,
+ 'job' => "FS::part_export::sqlmail::sqlmail_$method",
+ };
+ $queue->insert(
+ $self->option('datasrc'),
+ $self->option('username'),
+ $self->option('password'),
+ @_,
+ );
+}
+
+sub sqlmail_insert { #subroutine, not method
+ my $dbh = sqlmail_connect(shift, shift, shift);
+ my( $table, @attrib ) = @_;
+
+ my $sth = $dbh->prepare(
+ "INSERT INTO $table (" . join (',', @{$fields{$table}}) .
+ ") VALUES ('" . join ("','", @attrib) . "')"
+ ) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+
+ $dbh->disconnect;
+}
+
+sub sqlmail_delete { #subroutine, not method
+ my $dbh = sqlmail_connect(shift, shift, shift);
+ my( $table, $svcnum ) = @_;
+
+ my $sth = $dbh->prepare(
+ "DELETE FROM $table WHERE svcnum = $svcnum"
+ ) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+
+ $dbh->disconnect;
+}
+
+sub sqlmail_replace {
+ my $dbh = sqlmail_connect(shift, shift, shift);
+ my( $table, $svcnum, @attrib ) = @_;
+
+ my %data;
+ @data{@{$fields{$table}}} = @attrib;
+
+ my $sth = $dbh->prepare(
+ "UPDATE $table SET " .
+ ( join ',', map {$_ . "='" . $data{$_} . "'"} keys(%data) ) .
+ " WHERE svcnum = $svcnum"
+ ) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+
+ $dbh->disconnect;
+}
+
+sub sqlmail_connect {
+ #my($datasrc, $username, $password) = @_;
+ #DBI->connect($datasrc, $username, $password) or die $DBI::errstr;
+ DBI->connect(@_) or die $DBI::errstr;
+}
+
diff --git a/FS/FS/part_export/sysvshell.pm b/FS/FS/part_export/sysvshell.pm
new file mode 100644
index 000000000..f3f6b34b6
--- /dev/null
+++ b/FS/FS/part_export/sysvshell.pm
@@ -0,0 +1,7 @@
+package FS::part_export::sysvshell;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/textradius.pm b/FS/FS/part_export/textradius.pm
index 9a0468f6d..1492f2672 100644
--- a/FS/FS/part_export/textradius.pm
+++ b/FS/FS/part_export/textradius.pm
@@ -1,33 +1,37 @@
package FS::part_export::textradius;
-use vars qw(@ISA);
+use vars qw(@ISA $prefix);
+use Fcntl qw(:flock);
+use FS::UID qw(datasrc);
use FS::part_export;
@ISA = qw(FS::part_export);
+$prefix = "/usr/local/etc/freeside/export.";
+
sub rebless { shift; }
sub _export_insert {
my($self, $svc_acct) = (shift, shift);
$err_or_queue = $self->textradius_queue( $svc_acct->svcnum, 'insert',
- $svc_acct->username, $svc_acct->_password );
+ $svc_acct->username, $svc_acct->radius_check, '-', $svc_acct->radius_reply);
ref($err_or_queue) ? '' : $err_or_queue;
}
sub _export_replace {
my( $self, $new, $old ) = (shift, shift, shift);
- #return "can't change username with textradius"
- # if $old->username ne $new->username;
+ return "can't (yet?) change username with textradius"
+ if $old->username ne $new->username;
#return '' unless $old->_password ne $new->_password;
- $err_or_queue = $self->textradius_queue( $new->svcnum,
- 'replace', $new->username, $new->_password );
+ $err_or_queue = $self->textradius_queue( $new->svcnum, 'insert',
+ $new->username, $new->radius_check, '-', $new->radius_reply);
ref($err_or_queue) ? '' : $err_or_queue;
}
sub _export_delete {
my( $self, $svc_acct ) = (shift, shift);
- $err_or_queue = $self->textradius_queue( $svc_acct->svcnum,
- 'delete', $svc_acct->username );
+ $err_or_queue = $self->textradius_queue( $svc_acct->svcnum, 'delete',
+ $svc_acct->username );
ref($err_or_queue) ? '' : $err_or_queue;
}
@@ -38,13 +42,125 @@ sub textradius_queue {
'svcnum' => $svcnum,
'job' => "FS::part_export::textradius::textradius_$method",
};
- $queue->insert( @_ ) or $queue;
+ $queue->insert(
+ $self->option('user')||'root',
+ $self->machine,
+ $self->option('users'),
+ @_,
+ ) or $queue;
}
sub textradius_insert { #subroutine, not method
+ my( $user, $host, $users, $username, @attributes ) = @_;
+
+ #silly arg processing
+ my($att, @check);
+ push @check, $att while @attributes && ($att=shift @attributes) ne '-';
+ my %check = @check;
+ my %reply = @attributes;
+
+ my $file = textradius_download($user, $host, $users);
+
+ eval "use RADIUS::UserFile;";
+ die $@ if $@;
+
+ my $userfile = new RADIUS::UserFile(
+ File => $file,
+ Who => [ $username ],
+ Check_Items => [ keys %check ],
+ ) or die "error parsing $file";
+
+ $userfile->remove($username);
+ $userfile->add(
+ Who => $username,
+ Attributes => { %check, %reply },
+ Comment => 'user added by Freeside',
+ ) or die "error adding to $file";
+
+ $userfile->update( Who => [ $username ] )
+ or die "error updating $file";
+
+ textradius_upload($user, $host, $users);
+
}
-sub textradius_replace { #subroutine, not method
-}
+
sub textradius_delete { #subroutine, not method
+ my( $user, $host, $users, $username ) = @_;
+
+ my $file = textradius_download($user, $host, $users);
+
+ eval "use RADIUS::UserFile;";
+ die $@ if $@;
+
+ my $userfile = new RADIUS::UserFile(
+ File => $file,
+ Who => [ $username ],
+ ) or die "error parsing $file";
+
+ $userfile->remove($username);
+
+ $userfile->update( Who => [ $username ] )
+ or die "error updating $file";
+
+ textradius_upload($user, $host, $users);
+}
+
+sub textradius_download {
+ my( $user, $host, $users ) = @_;
+
+ my $dir = $prefix. datasrc;
+ mkdir $dir, 0700 or die $! unless -d $dir;
+ $dir .= "/$host";
+ mkdir $dir, 0700 or die $! unless -d $dir;
+
+ my $dest = "$dir/users";
+
+ eval "use File::Rsync;";
+ die $@ if $@;
+ my $rsync = File::Rsync->new({ rsh => 'ssh' });
+
+ open(LOCK, "+>>$dest.lock")
+ and flock(LOCK,LOCK_EX)
+ or die "can't open $dest.lock: $!";
+
+ $rsync->exec( {
+ src => "$user\@$host:$users",
+ dest => $dest,
+ } ); # true/false return value from exec is not working, alas
+ if ( $rsync->err ) {
+ die "error downloading $user\@$host:$users : ".
+ 'exit status: '. $rsync->status. ', '.
+ 'STDERR: '. join(" / ", $rsync->err). ', '.
+ 'STDOUT: '. join(" / ", $rsync->out);
+ }
+
+ $dest;
+}
+
+sub textradius_upload {
+ my( $user, $host, $users ) = @_;
+
+ my $dir = $prefix. datasrc. "/$host";
+
+ eval "use File::Rsync;";
+ die $@ if $@;
+ my $rsync = File::Rsync->new({
+ rsh => 'ssh',
+ #dry_run => 1,
+ });
+ $rsync->exec( {
+ src => "$dir/users",
+ dest => "$user\@$host:$users",
+ } ); # true/false return value from exec is not working, alas
+ if ( $rsync->err ) {
+ die "error uploading to $user\@$host:$users : ".
+ 'exit status: '. $rsync->status. ', '.
+ 'STDERR: '. join(" / ", $rsync->err). ', '.
+ 'STDOUT: '. join(" / ", $rsync->out);
+ }
+
+ flock(LOCK,LOCK_UN);
+ close LOCK;
+
}
diff --git a/FS/FS/part_export/vpopmail.pm b/FS/FS/part_export/vpopmail.pm
index 6a486faa1..561e2742a 100644
--- a/FS/FS/part_export/vpopmail.pm
+++ b/FS/FS/part_export/vpopmail.pm
@@ -1,6 +1,7 @@
package FS::part_export::vpopmail;
-use vars qw(@ISA @saltset $exportdir $rsync $ssh);
+use vars qw(@ISA @saltset $exportdir);
+use Fcntl qw(:flock);
use File::Path;
use FS::UID qw( datasrc );
use FS::part_export;
@@ -9,9 +10,6 @@ use FS::part_export;
@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-$rsync = "rsync";
-$ssh = "ssh";
-
sub rebless { shift; }
sub _export_insert {
@@ -20,6 +18,7 @@ sub _export_insert {
$svc_acct->username,
crypt($svc_acct->_password,$saltset[int(rand(64))].$saltset[int(rand(64))]),
$svc_acct->domain,
+ $svc_acct->quota,
);
}
@@ -47,7 +46,7 @@ sub _export_replace {
return '' unless $old->_password ne $new->_password;
$self->vpopmail_queue( $new->svcnum, 'replace',
- $new->username, $cpassword, $new->domain );
+ $new->username, $cpassword, $new->domain, $new->quota );
}
sub _export_delete {
@@ -59,14 +58,22 @@ sub _export_delete {
#a good idea to queue anything that could fail or take any time
sub vpopmail_queue {
my( $self, $svcnum, $method ) = (shift, shift, shift);
+
my $exportdir = "/usr/local/etc/freeside/export." . datasrc;
+ mkdir $exportdir, 0700 or die $! unless -d $exportdir;
+ $exportdir .= "/vpopmail";
+ mkdir $exportdir, 0700 or die $! unless -d $exportdir;
+ $exportdir .= '/'. $self->machine;
+ mkdir $exportdir, 0700 or die $! unless -d $exportdir;
+ mkdir "$exportdir/domains", 0700 or die $! unless -d "$exportdir/domains";
+
my $queue = new FS::queue {
'svcnum' => $svcnum,
'job' => "FS::part_export::vpopmail::vpopmail_$method",
};
$queue->insert(
$exportdir,
- $self->option('machine'),
+ $self->machine,
$self->option('dir'),
$self->option('uid'),
$self->option('gid'),
@@ -76,8 +83,11 @@ sub vpopmail_queue {
sub vpopmail_insert { #subroutine, not method
my( $exportdir, $machine, $dir, $uid, $gid ) = splice @_,0,5;
- my( $username, $password, $domain ) = @_;
-
+ my( $username, $password, $domain, $quota ) = @_;
+
+ mkdir "$exportdir/domains/$domain", 0700 or die $!
+ unless -d "$exportdir/domains/$domain";
+
(open(VPASSWD, ">>$exportdir/domains/$domain/vpasswd")
and flock(VPASSWD,LOCK_EX)
) or die "can't open vpasswd file for $username\@$domain: ".
@@ -87,9 +97,9 @@ sub vpopmail_insert { #subroutine, not method
$password,
'1',
'0',
- $username,
+ $finger,
"$dir/domains/$domain/$username",
- 'NOQUOTA',
+ $quota ? $quota.'S' : 'NOQUOTA',
), "\n";
flock(VPASSWD,LOCK_UN);
@@ -118,10 +128,21 @@ sub vpopmail_replace { #subroutine, not method
or die "Can't open $exportdir/domains/$domain/vpasswd.tmp: $!";
while (<VPASSWD>) {
- my ($mailbox, $pw, @rest) = split(':', $_);
- print VPASSWDTMP $_ unless $username eq $mailbox;
- print VPASSWDTMP join (':', ($mailbox, $password, @rest))
- if $username eq $mailbox;
+ my ($mailbox, $pw, $vuid, $vgid, $vfinger, $vdir, $vquota, @rest) =
+ split(':', $_);
+ if ( $username ne $mailbox ) {
+ print VPASSWDTMP $_;
+ next
+ }
+ print VPASSWDTMP join (':',
+ $mailbox,
+ $password,
+ '1',
+ '0',
+ $finger,
+ $dir,
+ $quota ? $quota.'S' : 'NOQUOTA',
+ ), "\n";
}
close(VPASSWDTMP);
@@ -171,9 +192,28 @@ sub vpopmail_sync {
my( $exportdir, $machine, $dir, $uid, $gid ) = splice @_,0,5;
chdir $exportdir;
- my @args = ( $rsync, "-rlpt", "-e", $ssh, "domains/",
- "vpopmail\@$machine:$dir/domains/" );
- system {$args[0]} @args;
+# my @args = ( $rsync, "-rlpt", "-e", $ssh, "domains/",
+# "vpopmail\@$machine:$dir/domains/" );
+# system {$args[0]} @args;
+
+ eval "use File::Rsync;";
+ die $@ if $@;
+
+ my $rsync = File::Rsync->new({ rsh => 'ssh' });
+
+ $rsync->exec( {
+ recursive => 1,
+ perms => 1,
+ times => 1,
+ src => "$exportdir/domains/",
+ dest => "vpopmail\@$machine:$dir/domains/",
+ } ); # true/false return value from exec is not working, alas
+ if ( $rsync->err ) {
+ die "error uploading to vpopmail\@$machine:$dir/domains/ : ".
+ 'exit status: '. $rsync->status. ', '.
+ 'STDERR: '. join(" / ", $rsync->err). ', '.
+ 'STDOUT: '. join(" / ", $rsync->out);
+ }
}
diff --git a/FS/FS/part_export/www_shellcommands.pm b/FS/FS/part_export/www_shellcommands.pm
new file mode 100644
index 000000000..e5b95dc1f
--- /dev/null
+++ b/FS/FS/part_export/www_shellcommands.pm
@@ -0,0 +1,114 @@
+package FS::part_export::www_shellcommands;
+
+use strict;
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+ my($self) = shift;
+ $self->_export_command('useradd', @_);
+}
+
+sub _export_delete {
+ my($self) = shift;
+ $self->_export_command('userdel', @_);
+}
+
+sub _export_command {
+ my ( $self, $action, $svc_www) = (shift, shift, shift);
+ my $command = $self->option($action);
+
+ #set variable for the command
+ no strict 'vars';
+ {
+ no strict 'refs';
+ ${$_} = $svc_www->getfield($_) foreach $svc_www->fields;
+ }
+ my $domain_record = $svc_www->domain_record; # or die ?
+ my $zone = $domain_record->reczone; # or die ?
+ unless ( $zone =~ /\.$/ ) {
+ my $svc_domain = $domain_record->svc_domain; # or die ?
+ $zone .= '.'. $svc_domain->domain;
+ }
+
+ my $svc_acct = $svc_www->svc_acct; # or die ?
+ my $username = $svc_acct->username;
+ my $homedir = $svc_acct->dir; # or die ?
+
+ #done setting variables for the command
+
+ $self->shellcommands_queue( $svc_www->svcnum,
+ user => $self->option('user')||'root',
+ host => $self->machine,
+ command => eval(qq("$command")),
+ );
+}
+
+sub _export_replace {
+ my($self, $new, $old ) = (shift, shift, shift);
+ my $command = $self->option('usermod');
+
+ #set variable for the command
+ no strict 'vars';
+ {
+ no strict 'refs';
+ ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+ ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+ }
+ my $old_domain_record = $old->domain_record; # or die ?
+ my $old_zone = $old_domain_record->reczone; # or die ?
+ unless ( $old_zone =~ /\.$/ ) {
+ my $old_svc_domain = $old_domain_record->svc_domain; # or die ?
+ $old_zone .= '.'. $old_svc_domain->domain;
+ }
+
+ my $old_svc_acct = $old->svc_acct; # or die ?
+ my $old_username = $old_svc_acct->username;
+ my $old_homedir = $old_svc_acct->dir; # or die ?
+
+ my $new_domain_record = $new->domain_record; # or die ?
+ my $new_zone = $new_domain_record->reczone; # or die ?
+ unless ( $new_zone =~ /\.$/ ) {
+ my $new_svc_domain = $new_domain_record->svc_domain; # or die ?
+ $new_zone .= '.'. $new_svc_domain->domain;
+ }
+
+ my $new_svc_acct = $new->svc_acct; # or die ?
+ my $new_username = $new_svc_acct->username;
+ my $new_homedir = $new_svc_acct->dir; # or die ?
+
+ #done setting variables for the command
+
+ $self->shellcommands_queue( $new->svcnum,
+ user => $self->option('user')||'root',
+ host => $self->machine,
+ command => eval(qq("$command")),
+ );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub shellcommands_queue {
+ my( $self, $svcnum ) = (shift, shift);
+ my $queue = new FS::queue {
+ 'svcnum' => $svcnum,
+ 'job' => "FS::part_export::www_shellcommands::ssh_cmd",
+ };
+ $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+ use Net::SSH '0.07';
+ &Net::SSH::ssh_cmd( { @_ } );
+}
+
+#sub shellcommands_insert { #subroutine, not method
+#}
+#sub shellcommands_replace { #subroutine, not method
+#}
+#sub shellcommands_delete { #subroutine, not method
+#}
+
diff --git a/FS/FS/part_export_option.pm b/FS/FS/part_export_option.pm
index 61ea956ae..a0b19fde1 100644
--- a/FS/FS/part_export_option.pm
+++ b/FS/FS/part_export_option.pm
@@ -106,7 +106,7 @@ sub check {
$self->ut_numbern('optionnum')
|| $self->ut_number('exportnum')
|| $self->ut_alpha('optionname')
- || $self->ut_textn('optionvalue')
+ || $self->ut_anything('optionvalue')
;
return $error if $error;
diff --git a/FS/FS/queue.pm b/FS/FS/queue.pm
index df92c5654..d35dc883f 100644
--- a/FS/FS/queue.pm
+++ b/FS/FS/queue.pm
@@ -1,7 +1,7 @@
package FS::queue;
use strict;
-use vars qw( @ISA @EXPORT_OK $conf );
+use vars qw( @ISA @EXPORT_OK $conf $jobnums);
use Exporter;
use FS::UID;
use FS::Conf;
@@ -18,6 +18,8 @@ $FS::UID::callback{'FS::queue'} = sub {
$conf = new FS::Conf;
};
+$jobnums = '';
+
=head1 NAME
FS::queue - Object methods for queue records
@@ -118,6 +120,8 @@ sub insert {
}
}
+ push @$jobnums, $self->jobnum if $jobnums;
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
@@ -192,7 +196,7 @@ sub check {
|| $self->ut_anything('job')
|| $self->ut_numbern('_date')
|| $self->ut_enum('status',['', qw( new locked failed )])
- || $self->ut_textn('statustext')
+ || $self->ut_anything('statustext')
|| $self->ut_numbern('svcnum')
;
return $error if $error;
@@ -257,10 +261,10 @@ in a database transaction.
sub depend_insert {
my($self, $other_jobnum) = @_;
- my $queue_depend = new FS::queue_depend (
+ my $queue_depend = new FS::queue_depend ( {
'jobnum' => $self->jobnum,
'depend_jobnum' => $other_jobnum,
- );
+ } );
$queue_depend->insert;
}
@@ -278,6 +282,7 @@ sub joblisting {
my($hashref, $noactions) = @_;
use Date::Format;
+ use HTML::Entities;
use FS::CGI;
my @queue = qsearch( 'queue', $hashref );
@@ -308,7 +313,9 @@ END
my $args;
if ( $dangerous || $queue->job !~ /^FS::part_export::/ || !$noactions ) {
- $args = join(' ', $queue->args);
+ $args = encode_entities( join(' ',
+ map { length($_)<54 ? $_ : substr($_,0,32)."..." } $queue->args #1&g
+ ) );
} else {
$args = '';
}
@@ -318,7 +325,7 @@ END
$status .= ': '. $queue->statustext if $queue->statustext;
my @queue_depend = $queue->queue_depend;
$status .= ' (waiting for '.
- join(', ', map { $_->other_jobnum } @queue_depend ).
+ join(', ', map { $_->depend_jobnum } @queue_depend ).
')'
if @queue_depend;
my $changable = $dangerous
@@ -378,10 +385,12 @@ END
=head1 VERSION
-$Id: queue.pm,v 1.13 2002-05-15 14:00:32 ivan Exp $
+$Id: queue.pm,v 1.15 2002-07-02 06:48:59 ivan Exp $
=head1 BUGS
+$jobnums global
+
=head1 SEE ALSO
L<FS::Record>, schema.html from the base documentation.
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index ee190fb8d..87b6097aa 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -1,10 +1,11 @@
package FS::svc_Common;
use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $noexport_hack );
use FS::Record qw( qsearchs fields dbh );
use FS::cust_svc;
use FS::part_svc;
+use FS::queue;
@ISA = qw( FS::Record );
@@ -27,7 +28,7 @@ inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record.
=over 4
-=item insert
+=item insert [ JOBNUM_ARRAYREF ]
Adds this record to the database. If there is an error, returns the error,
otherwise returns false.
@@ -35,10 +36,14 @@ otherwise returns false.
The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
defined. An FS::cust_svc record will be created and inserted.
+If an arrayref is passed as parameter, the B<jobnum>s of any export jobs will
+be added to the array.
+
=cut
sub insert {
my $self = shift;
+ local $FS::queue::jobnums = shift if @_;
my $error;
local $SIG{HUP} = 'IGNORE';
@@ -85,6 +90,18 @@ sub insert {
return $error;
}
+ #new-style exports!
+ unless ( $noexport_hack ) {
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+ my $error = $part_export->export_insert($self);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
@@ -112,16 +129,80 @@ sub delete {
my $svcnum = $self->svcnum;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
$error = $self->SUPER::delete;
return $error if $error;
+ #new-style exports!
+ unless ( $noexport_hack ) {
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+ my $error = $part_export->export_delete($self);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+ }
+
+ return $error if $error;
+
my $cust_svc = $self->cust_svc;
$error = $cust_svc->delete;
return $error if $error;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub replace {
+ my ($new, $old) = (shift, shift);
+
+ 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 $error = $new->SUPER::replace($old);
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ #new-style exports!
+ unless ( $noexport_hack ) {
+ foreach my $part_export ( $new->cust_svc->part_svc->part_export ) {
+ my $error = $part_export->export_replace($new,$old);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
+
=item setfixed
Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
@@ -198,24 +279,92 @@ sub cust_svc {
=item suspend
+Runs export_suspend callbacks.
+
+=cut
+
+sub suspend {
+ my $self = shift;
+
+ 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;
+
+ #new-style exports!
+ unless ( $noexport_hack ) {
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+ my $error = $part_export->export_suspend($self);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
=item unsuspend
+Runs export_unsuspend callbacks.
+
+=cut
+
+sub unsuspend {
+ my $self = shift;
+
+ 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;
+
+ #new-style exports!
+ unless ( $noexport_hack ) {
+ foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+ my $error = $part_export->export_unsuspend($self);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
=item cancel
-Stubs - return false (no error) so derived classes don't need to define these
+Stub - returns false (no error) so derived classes don't need to define these
methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
=cut
-sub suspend { ''; }
-sub unsuspend { ''; }
sub cancel { ''; }
=back
=head1 VERSION
-$Id: svc_Common.pm,v 1.8 2002-03-18 16:05:35 ivan Exp $
+$Id: svc_Common.pm,v 1.12 2002-06-14 11:22:53 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index a43f97ab5..e62cdd7bb 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -8,6 +8,8 @@ use vars qw( @ISA $noexport_hack $conf
$username_noperiod $username_nounderscore $username_nodash
$username_uppercase
$mydomain
+ $welcome_template $welcome_from $welcome_subject $welcome_mimetype
+ $smtpmachine
$dirhash
@saltset @pw_set );
use Carp;
@@ -17,6 +19,7 @@ use FS::Conf;
use FS::Record qw( qsearch qsearchs fields dbh );
use FS::svc_Common;
use Net::SSH;
+use FS::cust_svc;
use FS::part_svc;
use FS::svc_acct_pop;
use FS::svc_acct_sm;
@@ -25,6 +28,8 @@ use FS::svc_domain;
use FS::raddb;
use FS::queue;
use FS::radius_usergroup;
+use FS::export_svc;
+use FS::part_export;
use FS::Msgcat qw(gettext);
@ISA = qw( FS::svc_Common );
@@ -46,8 +51,19 @@ $FS::UID::callback{'FS::svc_acct'} = sub {
$username_uppercase = $conf->exists('username-uppercase');
$username_ampersand = $conf->exists('username-ampersand');
$mydomain = $conf->config('domain');
-
$dirhash = $conf->config('dirhash') || 0;
+ if ( $conf->exists('welcome_email') ) {
+ $welcome_template = new Text::Template (
+ TYPE => 'ARRAY',
+ SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
+ ) 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_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
+ } else {
+ $welcome_template = '';
+ }
+ $smtpmachine = $conf->config('smtpmachine');
};
@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
@@ -191,10 +207,13 @@ sub insert {
$error = $self->check;
return $error if $error;
- return gettext('username_in_use'). ": ". $self->username
- if qsearchs( 'svc_acct', { 'username' => $self->username,
- 'domsvc' => $self->domsvc,
- } );
+ #no, duplicate checking just got a whole lot more complicated
+ #(perhaps keep this check with a config option to turn on?)
+
+ #return gettext('username_in_use'). ": ". $self->username
+ # if qsearchs( 'svc_acct', { 'username' => $self->username,
+ # 'domsvc' => $self->domsvc,
+ # } );
if ( $self->svcnum ) {
my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
@@ -206,16 +225,92 @@ sub insert {
$self->svcpart($cust_svc->svcpart);
}
- my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
- return "Unknown svcpart" unless $part_svc;
- return "uid in use"
- if $part_svc->part_svc_column('uid')->columnflag ne 'F'
- && qsearchs( 'svc_acct', { 'uid' => $self->uid } )
- && $self->username !~ /^(hyla)?fax$/
- && $self->username !~ /^toor$/ #FreeBSD
- ;
+ #new duplicate username checking
+
+ my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
+ unless ( $part_svc ) {
+ $dbh->rollback if $oldAutoCommit;
+ return 'unknown svcpart '. $self->svcpart;
+ }
+
+ my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
+ my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
+ 'domsvc' => $self->domsvc } );
+ my @dup_uid;
+ if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
+ && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
+ @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
+ } else {
+ @dup_uid = ();
+ }
+
+ if ( @dup_user || @dup_userdomain || @dup_uid ) {
+ my $exports = FS::part_export::export_info('svc_acct');
+ my %conflict_user_svcpart;
+ my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
+
+ foreach my $part_export ( $part_svc->part_export ) {
+
+ #this will catch to the same exact export
+ my @svcparts = map { $_->svcpart }
+ qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
+
+ #this will catch to exports w/same exporthost+type ???
+ #my @other_part_export = qsearch('part_export', {
+ # 'machine' => $part_export->machine,
+ # 'exporttype' => $part_export->exporttype,
+ #} );
+ #foreach my $other_part_export ( @other_part_export ) {
+ # push @svcparts, map { $_->svcpart }
+ # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
+ #}
+
+ my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
+ if ( $nodomain =~ /^Y/i ) {
+ $conflict_user_svcpart{$_} = $part_export->exportnum
+ foreach @svcparts;
+ } else {
+ $conflict_userdomain_svcpart{$_} = $part_export->exportnum
+ foreach @svcparts;
+ }
+ }
+
+ foreach my $dup_user ( @dup_user ) {
+ my $dup_svcpart = $dup_user->cust_svc->svcpart;
+ if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
+ " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
+ }
+ }
+
+ foreach my $dup_userdomain ( @dup_userdomain ) {
+ my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
+ if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "duplicate username\@domain: conflicts with svcnum ".
+ $dup_userdomain->svcnum. " via exportnum ".
+ $conflict_userdomain_svcpart{$dup_svcpart};
+ }
+ }
+
+ foreach my $dup_uid ( @dup_uid ) {
+ my $dup_svcpart = $dup_uid->cust_svc->svcpart;
+ if ( exists($conflict_user_svcpart{$dup_svcpart})
+ || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
+ "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
+ || $conflict_userdomain_svcpart{$dup_svcpart};
+ }
+ }
+
+ }
- $error = $self->SUPER::insert;
+ #see? i told you it was more complicated
+
+ my @jobnums;
+ $error = $self->SUPER::insert(\@jobnums);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -235,16 +330,57 @@ sub insert {
}
}
- #new-style exports!
- unless ( $noexport_hack ) {
- foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
- my $error = $part_export->export_insert($self);
+ #false laziness with sub replace (and cust_main)
+ my $queue = new FS::queue {
+ 'svcnum' => $self->svcnum,
+ 'job' => 'FS::svc_acct::append_fuzzyfiles'
+ };
+ $error = $queue->insert($self->username);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "queueing job (transaction rolled back): $error";
+ }
+
+ #welcome email
+ my $cust_pkg = $self->cust_svc->cust_pkg;
+ my( $cust_main, $to ) = ( '', '' );
+ if ( $welcome_template && $cust_pkg ) {
+ my $cust_main = $cust_pkg->cust_main;
+ my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
+ if ( $to ) {
+ my $wqueue = new FS::queue {
+ 'svcnum' => $self->svcnum,
+ 'job' => 'FS::svc_acct::send_email'
+ };
+ warn "attempting to queue email to $to";
+ my $error = $wqueue->insert(
+ 'to' => $to,
+ 'from' => $welcome_from,
+ 'subject' => $welcome_subject,
+ 'mimetype' => $welcome_mimetype,
+ 'body' => $welcome_template->fill_in( HASH => {
+ 'username' => $self->username,
+ 'password' => $self->_password,
+ 'first' => $cust_main->first,
+ 'last' => $cust_main->getfield('last'),
+ 'pkg' => $cust_pkg->part_pkg->pkg,
+ } ),
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "exporting to ". $part_export->exporttype.
- " (transaction rolled back): $error";
+ return "queuing welcome email: $error";
+ }
+
+ foreach my $jobnum ( @jobnums ) {
+ my $error = $wqueue->depend_insert($jobnum);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "queuing welcome email job dependancy: $error";
+ }
}
+
}
+
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -332,26 +468,12 @@ sub delete {
}
}
- my $part_svc = $self->cust_svc->part_svc;
-
my $error = $self->SUPER::delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
- #new-style exports!
- unless ( $noexport_hack ) {
- foreach my $part_export ( $part_svc->part_export ) {
- my $error = $part_export->export_delete($self);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "exporting to ". $part_export->exporttype.
- " (transaction rolled back): $error";
- }
- }
- }
-
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
@@ -396,12 +518,6 @@ sub replace {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- $error = $new->SUPER::replace($old);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error if $error;
- }
-
$old->usergroup( [ $old->radius_groups ] );
if ( $new->usergroup ) {
#(sorta) false laziness with FS::part_export::sqlradius::_export_replace
@@ -436,18 +552,24 @@ sub replace {
}
- #new-style exports!
- unless ( $noexport_hack ) {
- foreach my $part_export ( $new->cust_svc->part_svc->part_export ) {
- my $error = $part_export->export_replace($new,$old);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "exporting to ". $part_export->exporttype.
- " (transaction rolled back): $error";
- }
- }
+ $error = $new->SUPER::replace($old);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error if $error;
}
+ #false laziness with sub insert (and cust_main)
+ my $queue = new FS::queue {
+ 'svcnum' => $new->svcnum,
+ 'job' => 'FS::svc_acct::append_fuzzyfiles'
+ };
+ $error = $queue->insert($new->username);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "queueing job (transaction rolled back): $error";
+ }
+
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
''; #no error
}
@@ -469,10 +591,11 @@ sub suspend {
) {
$hash{_password} = '*SUSPENDED* '.$hash{_password};
my $new = new FS::svc_acct ( \%hash );
- $new->replace($self);
- } else {
- ''; #no error (already suspended)
+ my $error = $new->replace($self);
+ return $error if $error;
}
+
+ $self->SUPER::suspend;
}
=item unsuspend
@@ -490,10 +613,11 @@ sub unsuspend {
if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
$hash{_password} = $1;
my $new = new FS::svc_acct ( \%hash );
- $new->replace($self);
- } else {
- ''; #no error (already unsuspended)
+ my $error = $new->replace($self);
+ return $error if $error;
}
+
+ $self->SUPER::unsuspend;
}
=item cancel
@@ -582,15 +706,9 @@ sub check {
&& $recref->{username} ne 'root'
&& $recref->{username} ne 'toor';
-# $error = $self->ut_textn('finger');
-# return $error if $error;
- $self->getfield('finger') =~
- /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\*\<\>]*)$/
- or return "Illegal finger: ". $self->getfield('finger');
- $self->setfield('finger', $1);
$recref->{dir} =~ /^([\/\w\-\.\&]*)$/
- or return "Illegal directory";
+ or return "Illegal directory: ". $recref->{dir};
$recref->{dir} = $1;
return "Illegal directory"
if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
@@ -622,22 +740,25 @@ sub check {
$recref->{shell} = '/bin/sync';
}
- $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota (unimplemented)";
- $recref->{quota} = $1;
-
} else {
$recref->{gid} ne '' ?
return "Can't have gid without uid" : ( $recref->{gid}='' );
- $recref->{finger} ne '' ?
- return "Can't have finger-name without uid" : ( $recref->{finger}='' );
$recref->{dir} ne '' ?
return "Can't have directory without uid" : ( $recref->{dir}='' );
$recref->{shell} ne '' ?
return "Can't have shell without uid" : ( $recref->{shell}='' );
- $recref->{quota} ne '' ?
- return "Can't have quota without uid" : ( $recref->{quota}='' );
}
+ # $error = $self->ut_textn('finger');
+ # return $error if $error;
+ $self->getfield('finger') =~
+ /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\*\<\>]*)$/
+ or return "Illegal finger: ". $self->getfield('finger');
+ $self->setfield('finger', $1);
+
+ $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
+ $recref->{quota} = $1;
+
unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
unless ( $recref->{slipip} eq '0e0' ) {
$recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
@@ -668,7 +789,7 @@ sub check {
#$recref->{password} = $1.
# crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
#;
- } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$]{13,34})$/ ) {
+ } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;]{13,34})$/ ) {
$recref->{_password} = $1.$3;
} elsif ( $recref->{_password} eq '*' ) {
$recref->{_password} = '*';
@@ -676,7 +797,7 @@ sub check {
$recref->{_password} = '!!';
} else {
#return "Illegal password";
- return gettext('illegal_password'). "$passwordmin-$passwordmax".
+ return gettext('illegal_password'). " $passwordmin-$passwordmax ".
FS::Msgcat::_gettext('illegal_password_characters').
": ". $recref->{_password};
}
@@ -715,8 +836,8 @@ sub radius_reply {
#$attrib =~ s/_/\-/g;
( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
} grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
- if ( $self->ip && $self->ip ne '0e0' ) {
- $reply{'Framed-IP-Address'} = $self->ip;
+ if ( $self->slipip && $self->slipip ne '0e0' ) {
+ $reply{'Framed-IP-Address'} = $self->slipip;
}
%reply;
}
@@ -765,7 +886,7 @@ sub domain {
=item svc_domain
Returns the FS::svc_domain record for this account's domain (see
-L<FS::svc_domain>.
+L<FS::svc_domain>).
=cut
@@ -820,14 +941,137 @@ Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
sub radius_groups {
my $self = shift;
- map { $_->groupname }
- qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
+ if ( $self->usergroup ) {
+ #when provisioning records, export callback runs in svc_Common.pm before
+ #radius_usergroup records can be inserted...
+ @{$self->usergroup};
+ } else {
+ map { $_->groupname }
+ qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
+ }
}
=back
=head1 SUBROUTINES
+=over 4
+
+=item send_email
+
+=cut
+
+sub send_email {
+ my %opt = @_;
+
+ use Date::Format;
+ use Mail::Internet 1.44;
+ use Mail::Header;
+
+ $opt{mimetype} ||= 'text/plain';
+ $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
+
+ $ENV{MAILADDRESS} = $opt{from};
+ my $header = new Mail::Header ( [
+ "From: $opt{from}",
+ "To: $opt{to}",
+ "Sender: $opt{from}",
+ "Reply-To: $opt{from}",
+ "Date: ". time2str("%a, %d %b %Y %X %z", time),
+ "Subject: $opt{subject}",
+ "Content-Type: $opt{mimetype}",
+ ] );
+ my $message = new Mail::Internet (
+ 'Header' => $header,
+ 'Body' => [ map "$_\n", split("\n", $opt{body}) ],
+ );
+ $!=0;
+ $message->smtpsend( Host => $smtpmachine )
+ or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+ or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!";
+}
+
+=item check_and_rebuild_fuzzyfiles
+
+=cut
+
+sub check_and_rebuild_fuzzyfiles {
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ -e "$dir/svc_acct.username"
+ or &rebuild_fuzzyfiles;
+}
+
+=item rebuild_fuzzyfiles
+
+=cut
+
+sub rebuild_fuzzyfiles {
+
+ use Fcntl qw(:flock);
+
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+
+ #username
+
+ open(USERNAMELOCK,">>$dir/svc_acct.username")
+ or die "can't open $dir/svc_acct.username: $!";
+ flock(USERNAMELOCK,LOCK_EX)
+ or die "can't lock $dir/svc_acct.username: $!";
+
+ my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
+
+ open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
+ or die "can't open $dir/svc_acct.username.tmp: $!";
+ print USERNAMECACHE join("\n", @all_username), "\n";
+ close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
+
+ rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
+ close USERNAMELOCK;
+
+}
+
+=item all_username
+
+=cut
+
+sub all_username {
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ open(USERNAMECACHE,"<$dir/svc_acct.username")
+ or die "can't open $dir/svc_acct.username: $!";
+ my @array = map { chomp; $_; } <USERNAMECACHE>;
+ close USERNAMECACHE;
+ \@array;
+}
+
+=item append_fuzzyfiles USERNAME
+
+=cut
+
+sub append_fuzzyfiles {
+ my $username = shift;
+
+ &check_and_rebuild_fuzzyfiles;
+
+ use Fcntl qw(:flock);
+
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+
+ open(USERNAME,">>$dir/svc_acct.username")
+ or die "can't open $dir/svc_acct.username: $!";
+ flock(USERNAME,LOCK_EX)
+ or die "can't lock $dir/svc_acct.username: $!";
+
+ print USERNAME "$username\n";
+
+ flock(USERNAME,LOCK_UN)
+ or die "can't unlock $dir/svc_acct.username: $!";
+ close USERNAME;
+
+ 1;
+}
+
+
+
=item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
=cut
@@ -877,6 +1121,8 @@ END
$html;
}
+=back
+
=head1 BUGS
The $recref stuff in sub check should be cleaned up.
diff --git a/FS/FS/svc_domain.pm b/FS/FS/svc_domain.pm
index a9a2fd0eb..b06d03013 100644
--- a/FS/FS/svc_domain.pm
+++ b/FS/FS/svc_domain.pm
@@ -282,6 +282,7 @@ sub delete {
return $error;
}
}
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
}
=item replace OLD_RECORD
@@ -293,13 +294,12 @@ returns the error, otherwise returns false.
sub replace {
my ( $new, $old ) = ( shift, shift );
- my $error;
return "Can't change domain - reorder."
if $old->getfield('domain') ne $new->getfield('domain');
- $new->SUPER::replace($old);
-
+ my $error = $new->SUPER::replace($old);
+ return $error if $error;
}
=item suspend
@@ -450,7 +450,7 @@ sub submit_internic {
=head1 VERSION
-$Id: svc_domain.pm,v 1.29 2002-05-22 18:44:01 ivan Exp $
+$Id: svc_domain.pm,v 1.31 2002-06-10 02:52:48 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/svc_forward.pm b/FS/FS/svc_forward.pm
index 12f8b9236..1c5b5c40d 100644
--- a/FS/FS/svc_forward.pm
+++ b/FS/FS/svc_forward.pm
@@ -402,7 +402,7 @@ sub check {
return "Unknown srcsvc" unless $self->srcsvc_acct;
- return "Both dstsvc and dst were defined; one one can be specified"
+ return "Both dstsvc and dst were defined; only one can be specified"
if $self->dstsvc && $self->dst;
return "one of dstsvc or dst is required"
@@ -452,7 +452,7 @@ sub dstsvc_acct {
=head1 VERSION
-$Id: svc_forward.pm,v 1.11 2002-02-20 01:03:09 ivan Exp $
+$Id: svc_forward.pm,v 1.12 2002-05-31 17:50:37 ivan Exp $
=head1 BUGS
diff --git a/FS/FS/svc_www.pm b/FS/FS/svc_www.pm
index f09a3f89d..d7a42c8ae 100644
--- a/FS/FS/svc_www.pm
+++ b/FS/FS/svc_www.pm
@@ -1,7 +1,7 @@
package FS::svc_www;
use strict;
-use vars qw(@ISA $conf $apacheroot $apachemachine $apacheip $nossh_hack );
+use vars qw(@ISA $conf $apacheip);
#use FS::Record qw( qsearch qsearchs );
use FS::Record qw( qsearchs dbh );
use FS::svc_Common;
@@ -9,15 +9,12 @@ use FS::cust_svc;
use FS::domain_record;
use FS::svc_acct;
use FS::svc_domain;
-use Net::SSH qw(ssh);
@ISA = qw( FS::svc_Common );
#ask FS::UID to run this stuff for us later
$FS::UID::callback{'FS::svc_www'} = sub {
$conf = new FS::Conf;
- $apacheroot = $conf->config('apacheroot');
- $apachemachine = $conf->config('apachemachine');
$apacheip = $conf->config('apacheip');
};
@@ -85,20 +82,6 @@ otherwise returns false.
The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
defined. An FS::cust_svc record will be created and inserted.
-If the configuration values (see L<FS::Conf>) I<apachemachine>, and
-I<apacheroot> exist, the command:
-
- mkdir $apacheroot/$zone;
- chown $username $apacheroot/$zone;
- ln -s $apacheroot/$zone $homedir/$zone
-
-I<$zone> is the DNS A record pointed to by I<recnum>
-I<$username> is the username pointed to by I<usersvc>
-I<$homedir> is that user's home directory
-
-is executed on I<apachemachine> via ssh. This behaviour can be surpressed by
-setting $FS::svc_www::nossh_hack true.
-
=cut
sub insert {
@@ -147,37 +130,6 @@ sub insert {
return $error;
}
- my $domain_record = qsearchs('domain_record', { 'recnum' => $self->recnum } ); # or die ?
- my $zone = $domain_record->reczone;
- # or die ?
- unless ( $zone =~ /\.$/ ) {
- my $dom_svcnum = $domain_record->svcnum;
- my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $dom_svcnum } );
- # or die ?
- $zone .= $svc_domain->domain;
- }
-
- my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $self->usersvc } );
- # or die ?
- my $username = $svc_acct->username;
- # or die ?
- my $homedir = $svc_acct->dir;
- # or die ?
-
- if ( $apachemachine
- && $apacheroot
- && $zone
- && $username
- && $homedir
- && ! $nossh_hack
- ) {
- ssh("root\@$apachemachine",
- "mkdir $apacheroot/$zone; ".
- "chown $username $apacheroot/$zone; ".
- "ln -s $apacheroot/$zone $homedir/$zone"
- );
- }
-
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
@@ -285,6 +237,30 @@ sub check {
''; #no error
}
+=item domain_record
+
+Returns the FS::domain_record record for this web virtual host's zone (see
+L<FS::domain_record>).
+
+=cut
+
+sub domain_record {
+ my $self = shift;
+ qsearchs('domain_record', { 'recnum' => $self->recnum } );
+}
+
+=item svc_acct
+
+Returns the FS::svc_acct record for this web virtual host's owner (see
+L<FS::svc_acct>).
+
+=cut
+
+sub svc_acct {
+ my $self = shift;
+ qsearchs('svc_acct', { 'svcnum' => $self->usersvc } );
+}
+
=back
=head1 BUGS
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 4c6d243df..fff95c8c8 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -7,8 +7,11 @@ bin/freeside-bill
bin/freeside-daily
bin/freeside-email
bin/freeside-queued
+bin/freeside-addoutsource
+bin/freeside-addoutsourceuser
bin/freeside-apply-credits
bin/freeside-adduser
+bin/freeside-setup
bin/freeside-setinvoice
bin/freeside-overdue
bin/freeside-receivables-report
@@ -16,8 +19,13 @@ bin/freeside-tax-report
bin/freeside-cc-receipts-report
bin/freeside-credit-report
bin/freeside-expiration-alerter
+bin/freeside-reexport
FS.pm
FS/CGI.pm
+FS/InitHandler.pm
+FS/ClientAPI.pm
+FS/ClientAPI/passwd.pm
+FS/ClientAPI/MyAccount.pm
FS/Conf.pm
FS/ConfItem.pm
FS/Record.pm
@@ -49,14 +57,23 @@ FS/part_bill_event.pm
FS/export_svc.pm
FS/part_export.pm
FS/part_export_option.pm
+FS/part_export/bind.pm
+FS/part_export/bind_slave.pm
FS/part_export/bsdshell.pm
FS/part_export/cp.pm
FS/part_export/cyrus.pm
+FS/part_export/domain_shellcommands.pm
+FS/part_export/http.pm
FS/part_export/infostreet.pm
+FS/part_export/null.pm
FS/part_export/shellcommands.pm
+FS/part_export/shellcommands_withdomain.pm
+FS/part_export/sqlmail.pm
FS/part_export/sqlradius.pm
+FS/part_export/sysvshell.pm
FS/part_export/textradius.pm
FS/part_export/vpopmail.pm
+FS/part_export/www_shellcommands.pm
FS/part_pkg.pm
FS/part_pop_local.pm
FS/part_referral.pm
@@ -86,6 +103,8 @@ FS/cust_tax_exempt.pm
t/agent.t
t/agent_type.t
t/CGI.t
+t/InitHandler.t
+t/ClientAPI.t
t/Conf.t
t/ConfItem.t
t/Record.t
@@ -112,14 +131,23 @@ t/part_bill_event.t
t/export_svc.t
t/part_export.t
t/part_export_option.t
+t/part_export-bind.t
+t/part_export-bind_slave.t
t/part_export-bsdshell.t
t/part_export-cp.t
t/part_export-cyrus.t
+t/part_export-domain_shellcommands.t
+t/part_export-http.t
t/part_export-infostreet.t
+t/part_export-null.t
t/part_export-shellcommands.t
+t/part_export-shellcommands_withdomain.t
+t/part_export-sqlmail.t
t/part_export-sqlradius.t
+t/part_export-sysvshell.t
t/part_export-textradius.t
t/part_export-vpopmail.t
+t/part_export-www_shellcommands.t
t/part_pkg.t
t/part_pop_local.t
t/part_referral.t
diff --git a/FS/bin/freeside-addoutsource b/FS/bin/freeside-addoutsource
new file mode 100644
index 000000000..5cec17f46
--- /dev/null
+++ b/FS/bin/freeside-addoutsource
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+domain=$1
+
+createdb $domain && \
+\
+mkdir /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+\
+chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+\
+cp /home/ivan/freeside/conf/[a-z]* /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+\
+touch /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+chmod 600 /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+echo -e "DBI:Pg:host=localhost;dbname=$domain\nfreeside\n" >/usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+mkdir /usr/local/etc/freeside/counters.DBI:Pg:host=localhost\;dbname=$domain && \
+mkdir /usr/local/etc/freeside/cache.DBI:Pg:host=localhost\;dbname=$domain && \
+mkdir /usr/local/etc/freeside/export.DBI:Pg:host=localhost\;dbname=$domain
+
diff --git a/FS/bin/freeside-addoutsourceuser b/FS/bin/freeside-addoutsourceuser
new file mode 100644
index 000000000..bbad8aa3f
--- /dev/null
+++ b/FS/bin/freeside-addoutsourceuser
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+username=$1
+domain=$2
+password=$3
+
+freeside-adduser -h /usr/local/etc/freeside/htpasswd \
+ -s conf.DBI:Pg:host=localhost\;dbname=$domain/secrets \
+ -b \
+ $username $password 2>/dev/null
+
+[ -e /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain ] \
+ || ( freeside-setup $username 2>/dev/null; \
+ /home/ivan/freeside/bin/populate-msgcat $username; 2>/dev/null )
+
diff --git a/FS/bin/freeside-adduser b/FS/bin/freeside-adduser
index 9d424634b..424123226 100644
--- a/FS/bin/freeside-adduser
+++ b/FS/bin/freeside-adduser
@@ -1,21 +1,23 @@
#!/usr/bin/perl -w
#
-# $Id: freeside-adduser,v 1.4 2002-02-06 14:58:05 ivan Exp $
+# $Id: freeside-adduser,v 1.7 2002-08-25 01:16:30 ivan Exp $
use strict;
-use vars qw($opt_h $opt_c $opt_s);
+use vars qw($opt_h $opt_b $opt_c $opt_s);
use Getopt::Std;
my $FREESIDE_CONF = "/usr/local/etc/freeside";
-getopts("ch:s:");
+getopts("bch:s:");
die &usage if $opt_c && ! $opt_h;
my $user = shift or die &usage;
if ( $opt_h ) {
my @args = ( 'htpasswd' );
+ push @args, '-b' if $opt_b;
push @args, '-c' if $opt_c;
push @args, $opt_h, $user;
+ push @args, shift if $opt_b;
system(@args) == 0 or die "htpasswd failed: $?";
}
@@ -27,7 +29,7 @@ print MAPSECRETS "$user $secretfile\n";
close MAPSECRETS or die "can't close $FREESIDE_CONF/mapsecrets: $!";
sub usage {
- die "Usage:\n\n freeside-adduser [ -h htpasswd_file [ -c ] ] [ -s secretfile ] username"
+ die "Usage:\n\n freeside-adduser [ -h htpasswd_file [ -c ] [ -b ] ] [ -s secretfile ] username"
}
=head1 NAME
@@ -45,13 +47,15 @@ sales/tech folks) to the web interface, not for adding customer accounts.
-h: Also call htpasswd for this user with the given filename
- -c: Passed to htpasswd
+ -c: Passed to htpasswd(1)
-s: Specify an alternate secret file
+ -b: same as htpasswd(1), probably insecure, not recommended
+
=head1 SEE ALSO
-L<htpasswd>, base Freeside documentation
+L<htpasswd>(1), base Freeside documentation
=cut
diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily
index e6f02df33..142b0c73a 100755
--- a/FS/bin/freeside-daily
+++ b/FS/bin/freeside-daily
@@ -4,7 +4,7 @@ use strict;
use Fcntl qw(:flock);
use Date::Parse;
use Getopt::Std;
-use FS::UID qw(adminsuidsetup);
+use FS::UID qw(adminsuidsetup driver_name dbh);
use FS::Record qw(qsearch qsearchs);
use FS::cust_main;
@@ -41,6 +41,13 @@ foreach $cust_main ( @cust_main ) {
}
+if ( driver_name eq 'Pg' ) {
+ foreach my $statement ( 'vacuum', 'vacuum analyze' ) {
+ my $sth = dbh->prepare($statement) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ }
+}
+
# subroutines
sub untaint_argv {
diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued
index 1539a48af..311fe62f9 100644
--- a/FS/bin/freeside-queued
+++ b/FS/bin/freeside-queued
@@ -7,23 +7,25 @@ use Fcntl qw(:flock);
use POSIX qw(setsid);
use Date::Format;
use IO::File;
-use FS::UID qw(adminsuidsetup forksuidsetup driver_name);
-use FS::Record qw(qsearchs);
+use FS::UID qw(adminsuidsetup forksuidsetup driver_name dbh);
+use FS::Record qw(qsearch qsearchs);
use FS::queue;
+use FS::queue_depend;
# no autoloading just yet
use FS::cust_main;
use FS::svc_acct;
-use Net::SSH 0.05;
+use Net::SSH 0.07;
use FS::part_export;
-my $pid_file = '/var/run/freeside-queued.pid';
-
$max_kids = '10'; #guess it should be a config file...
$kids = 0;
my $user = shift or die &usage;
+#my $pid_file = "/var/run/freeside-queued.$user.pid";
+my $pid_file = "/var/run/freeside-queued.pid";
+
&daemonize1;
sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
@@ -34,8 +36,20 @@ $sigint = 0;
$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; };
$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; };
-$> = $FS::UID::freeside_uid unless $>;
-$< = $>;
+my $freeside_gid = scalar(getgrnam('freeside'))
+ or die "can't setgid to freeside group\n";
+$) = $freeside_gid;
+$( = $freeside_gid;
+#if freebsd can't setuid(), presumably it can't setgid() either. grr fleabsd
+($(,$)) = ($),$();
+$) = $freeside_gid;
+
+$> = $FS::UID::freeside_uid;
+$< = $FS::UID::freeside_uid;
+#freebsd is sofa king broken, won't setuid()
+($<,$>) = ($>,$<);
+$> = $FS::UID::freeside_uid;
+
$ENV{HOME} = (getpwuid($>))[7]; #for ssh
adminsuidsetup $user;
@@ -59,27 +73,49 @@ while (1) {
}
$warnkids=0;
- my $nodepend = 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'.
- ' WHERE queue_depend.jobnum = queue.jobnum ) ';
-
+ my $nodepend = driver_name eq 'mysql'
+ ? ''
+ : 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'.
+ ' WHERE queue_depend.jobnum = queue.jobnum ) ';
+
+ #my($job, $ljob);
+ #{
+ # my $oldAutoCommit = $FS::UID::AutoCommit;
+ # local $FS::UID::AutoCommit = 0;
+ $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
my $job = qsearchs(
'queue',
{ 'status' => 'new' },
'',
- driver_name =~ /^mysql$/i
+ driver_name eq 'mysql'
? "$nodepend ORDER BY jobnum LIMIT 1 FOR UPDATE"
: "$nodepend ORDER BY jobnum FOR UPDATE LIMIT 1"
) or do {
+ $dbh->commit or die $dbh->errstr; #if $oldAutoCommit;
sleep 5; #connecting to db is expensive
next;
};
+ if ( driver_name eq 'mysql'
+ && qsearch('queue_depend', { 'jobnum' => $job->jobnum } ) ) {
+ $dbh->commit or die $dbh->errstr; #if $oldAutoCommit;
+ sleep 5; #would be better if mysql could do everything in query above
+ next;
+ }
+
my %hash = $job->hash;
$hash{'status'} = 'locked';
my $ljob = new FS::queue ( \%hash );
my $error = $ljob->replace($job);
die $error if $error;
+ $dbh->commit or die $dbh->errstr; #if $oldAutoCommit;
+
+ $FS::UID::AutoCommit = 1;
+ #}
+
my @args = $ljob->args;
defined( my $pid = fork ) or do {
diff --git a/FS/bin/freeside-reexport b/FS/bin/freeside-reexport
new file mode 100644
index 000000000..b5c50a422
--- /dev/null
+++ b/FS/bin/freeside-reexport
@@ -0,0 +1,62 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::svc_acct;
+use FS::cust_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $export_x = shift or die &usage;
+my @part_export;
+if ( $export_x =~ /^(\d+)$/ ) {
+ @part_export = qsearchs('part_export', { exportnum=>$1 } )
+ or die "exportnum $export_x not found\n";
+} else {
+ @part_export = qsearch('part_export', { exporttype=>$export_x } )
+ or die "no exports of type $export_x found\n";
+}
+
+my $svc_something = shift or die &usage;
+my $svc_x;
+if ( $svc_something =~ /^(\d+)$/ ) {
+ my $cust_svc = qsearchs('cust_svc', { svcnum=>$1 } )
+ or die "svcnum $svc_something not found\n";
+ $svc_x = $cust_svc->svc_x;
+} else {
+ $svc_x = qsearchs('svc_acct', { username=>$svc_something } )
+ or die "username $svc_something not found\n";
+}
+
+foreach my $part_export ( @part_export ) {
+ my $error = $part_export->export_insert($svc_x);
+ die $error if $error;
+}
+
+
+sub usage {
+ die "Usage:\n\n freeside-reexport user exportnum|exporttype svcnum|username\n";
+}
+
+=head1 NAME
+
+freeside-reexport - Command line tool to re-trigger export jobs for existing services
+
+=head1 SYNOPSIS
+
+ freeside-reexport user exportnum|exporttype svcnum|username
+
+=head1 DESCRIPTION
+
+ Re-queues the export job for the specified exportnum or exporttype(s) and
+ specified service (selected by svcnum or username).
+
+=head1 SEE ALSO
+
+L<freeside-sqlradius-reset>, L<FS::part_export>
+
+=cut
+
diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup
new file mode 100755
index 000000000..78a03385c
--- /dev/null
+++ b/FS/bin/freeside-setup
@@ -0,0 +1,1038 @@
+#!/usr/bin/perl -Tw
+
+#to delay loading dbdef until we're ready
+BEGIN { $FS::Record::setup_hack = 1; }
+
+use strict;
+use vars qw($opt_s);
+use Getopt::Std;
+use DBI;
+use DBIx::DBSchema 0.20;
+use DBIx::DBSchema::Table;
+use DBIx::DBSchema::Column;
+use DBIx::DBSchema::ColGroup::Unique;
+use DBIx::DBSchema::ColGroup::Index;
+use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets);
+use FS::Record;
+use FS::cust_main_county;
+use FS::raddb;
+use FS::part_bill_event;
+
+die "Not running uid freeside!" unless checkeuid();
+
+my %attrib2db =
+ map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib;
+
+getopts("s");
+my $user = shift or die &usage;
+getsecrets($user);
+
+#needs to match FS::Record
+my($dbdef_file) = "/usr/local/etc/freeside/dbdef.". datasrc;
+
+###
+
+#print "\nEnter the maximum username length: ";
+#my($username_len)=&getvalue;
+my $username_len = 32; #usernamemax config file
+
+#print "\n\n", <<END, ":";
+#Freeside tracks the RADIUS User-Name, check attribute Password and
+#reply attribute Framed-IP-Address for each user. You can specify additional
+#check and reply attributes (or you can add them later with the
+#fs-radius-add-check and fs-radius-add-reply programs).
+#
+#First enter any additional RADIUS check attributes you need to track for each
+#user, separated by whitespace.
+#END
+#my @check_attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; }
+# split(" ",&getvalue);
+#
+#print "\n\n", <<END, ":";
+#Now enter any additional reply attributes you need to track for each user,
+#separated by whitespace.
+#END
+#my @attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; }
+# split(" ",&getvalue);
+#
+#print "\n\n", <<END, ":";
+#Do you wish to enable the tracking of a second, separate shipping/service
+#address?
+#END
+#my $ship = &_yesno;
+#
+#sub getvalue {
+# my($x)=scalar(<STDIN>);
+# chop $x;
+# $x;
+#}
+#
+#sub _yesno {
+# print " [y/N]:";
+# my $x = scalar(<STDIN>);
+# $x =~ /^y/i;
+#}
+
+my @check_attributes = (); #add later
+my @attributes = (); #add later
+my $ship = $opt_s;
+
+###
+
+my($char_d) = 80; #default maxlength for text fields
+
+#my(@date_type) = ( 'timestamp', '', '' );
+my(@date_type) = ( 'int', 'NULL', '' );
+my(@perl_type) = ( 'text', 'NULL', '' );
+my @money_type = ( 'decimal', '', '10,2' );
+
+###
+# create a dbdef object from the old data structure
+###
+
+my(%tables)=&tables_hash_hack;
+
+#turn it into objects
+my($dbdef) = new DBIx::DBSchema ( map {
+ my(@columns);
+ while (@{$tables{$_}{'columns'}}) {
+ my($name,$type,$null,$length)=splice @{$tables{$_}{'columns'}}, 0, 4;
+ push @columns, new DBIx::DBSchema::Column ( $name,$type,$null,$length );
+ }
+ DBIx::DBSchema::Table->new(
+ $_,
+ $tables{$_}{'primary_key'},
+ DBIx::DBSchema::ColGroup::Unique->new($tables{$_}{'unique'}),
+ DBIx::DBSchema::ColGroup::Index->new($tables{$_}{'index'}),
+ @columns,
+ );
+} (keys %tables) );
+
+my $cust_main = $dbdef->table('cust_main');
+unless ($ship) { #remove ship_ from cust_main
+ $cust_main->delcolumn($_) foreach ( grep /^ship_/, $cust_main->columns );
+} else { #add indices on ship_last and ship_company
+ push @{$cust_main->index->lol_ref}, ( ['ship_last'], ['ship_company'] )
+}
+
+#add radius attributes to svc_acct
+
+my($svc_acct)=$dbdef->table('svc_acct');
+
+my($attribute);
+foreach $attribute (@attributes) {
+ $svc_acct->addcolumn ( new DBIx::DBSchema::Column (
+ 'radius_'. $attribute,
+ 'varchar',
+ 'NULL',
+ $char_d,
+ ));
+}
+
+foreach $attribute (@check_attributes) {
+ $svc_acct->addcolumn( new DBIx::DBSchema::Column (
+ 'rc_'. $attribute,
+ 'varchar',
+ 'NULL',
+ $char_d,
+ ));
+}
+
+##make part_svc table (but now as object)
+#
+#my($part_svc)=$dbdef->table('part_svc');
+#
+##because of svc_acct_pop
+##foreach (grep /^svc_/, $dbdef->tables) {
+##foreach (qw(svc_acct svc_acct_sm svc_charge svc_domain svc_wo)) {
+#foreach (qw(svc_acct svc_domain svc_forward svc_www)) {
+# my($table)=$dbdef->table($_);
+# my($col);
+# foreach $col ( $table->columns ) {
+# next if $col =~ /^svcnum$/;
+# $part_svc->addcolumn( new DBIx::DBSchema::Column (
+# $table->name. '__' . $table->column($col)->name,
+# 'varchar', #$table->column($col)->type,
+# 'NULL',
+# $char_d, #$table->column($col)->length,
+# ));
+# $part_svc->addcolumn ( new DBIx::DBSchema::Column (
+# $table->name. '__'. $table->column($col)->name . "_flag",
+# 'char',
+# 'NULL',
+# 1,
+# ));
+# }
+#}
+
+#create history tables (false laziness w/create-history-tables)
+foreach my $table ( grep { ! /^h_/ } $dbdef->tables ) {
+ my $tableobj = $dbdef->table($table)
+ or die "unknown table $table";
+
+ die "unique->lol_ref undefined for $table"
+ unless defined $tableobj->unique->lol_ref;
+ die "index->lol_ref undefined for $table"
+ unless defined $tableobj->index->lol_ref;
+
+ my $h_tableobj = DBIx::DBSchema::Table->new( {
+ name => "h_$table",
+ primary_key => 'historynum',
+ unique => DBIx::DBSchema::ColGroup::Unique->new( [] ),
+ 'index' => DBIx::DBSchema::ColGroup::Index->new( [
+ @{$tableobj->unique->lol_ref},
+ @{$tableobj->index->lol_ref}
+ ] ),
+ columns => [
+ DBIx::DBSchema::Column->new( {
+ 'name' => 'historynum',
+ 'type' => 'serial',
+ 'null' => 'NOT NULL',
+ 'length' => '',
+ 'default' => '',
+ 'local' => '',
+ } ),
+ DBIx::DBSchema::Column->new( {
+ 'name' => 'history_date',
+ 'type' => 'int',
+ 'null' => 'NULL',
+ 'length' => '',
+ 'default' => '',
+ 'local' => '',
+ } ),
+ DBIx::DBSchema::Column->new( {
+ 'name' => 'history_user',
+ 'type' => 'varchar',
+ 'null' => 'NOT NULL',
+ 'length' => '80',
+ 'default' => '',
+ 'local' => '',
+ } ),
+ DBIx::DBSchema::Column->new( {
+ 'name' => 'history_action',
+ 'type' => 'varchar',
+ 'null' => 'NOT NULL',
+ 'length' => '80',
+ 'default' => '',
+ 'local' => '',
+ } ),
+ map { $tableobj->column($_) } $tableobj->columns
+ ],
+ } );
+ $dbdef->addtable($h_tableobj);
+}
+
+#important
+$dbdef->save($dbdef_file);
+&FS::Record::reload_dbdef($dbdef_file);
+
+###
+# create 'em
+###
+
+my($dbh)=adminsuidsetup $user;
+
+#create tables
+$|=1;
+
+foreach my $statement ( $dbdef->sql($dbh) ) {
+ $dbh->do( $statement )
+ or die "CREATE error: ". $dbh->errstr. "\ndoing statement: $statement";
+}
+
+#not really sample data (and shouldn't default to US)
+
+#cust_main_county
+
+#USPS state codes
+foreach ( qw(
+AL AK AS AZ AR CA CO CT DC DE FM FL GA GU HI ID IL IN IA KS KY LA
+ME MH MD MA MI MN MS MO MT NC ND NE NH NJ NM NV NY MP OH OK OR PA PW PR RI
+SC SD TN TX UT VT VI VA WA WV WI WY AE AA AP
+) ) {
+ my($cust_main_county)=new FS::cust_main_county({
+ 'state' => $_,
+ 'tax' => 0,
+ 'country' => 'US',
+ });
+ my($error);
+ $error=$cust_main_county->insert;
+ die $error if $error;
+}
+
+#AU "offical" state codes ala mark.williamson@ebbs.com.au (Mark Williamson)
+foreach ( qw(
+VIC NSW NT QLD TAS ACT WA SA
+) ) {
+ my($cust_main_county)=new FS::cust_main_county({
+ 'state' => $_,
+ 'tax' => 0,
+ 'country' => 'AU',
+ });
+ my($error);
+ $error=$cust_main_county->insert;
+ die $error if $error;
+}
+
+#ISO 2-letter country codes (same as country TLDs) except US and AU
+foreach ( qw(
+AF AL DZ AS AD AO AI AQ AG AR AM AW AT AZ BS BH BD BB BY BE BZ BJ BM BT BO
+BA BW BV BR IO BN BG BF BI KH CM CA CV KY CF TD CL CN CX CC CO KM CG CK CR CI
+HR CU CY CZ DK DJ DM DO TP EC EG SV GQ ER EE ET FK FO FJ FI FR FX GF PF TF GA
+GM GE DE GH GI GR GL GD GP GU GT GN GW GY HT HM HN HK HU IS IN ID IR IQ IE IL
+IT JM JP JO KZ KE KI KP KR KW KG LA LV LB LS LR LY LI LT LU MO MK MG MW MY MV
+ML MT MH MQ MR MU YT MX FM MD MC MN MS MA MZ MM NA NR NP NL AN NC NZ NI NE NG
+NU NF MP NO OM PK PW PA PG PY PE PH PN PL PT PR QA RE RO RU RW KN LC VC WS SM
+ST SA SN SC SL SG SK SI SB SO ZA GS ES LK SH PM SD SR SJ SZ SE CH SY TW TJ TZ
+TH TG TK TO TT TN TR TM TC TV UG UA AE GB UM UY UZ VU VA VE VN VG VI WF EH
+YE YU ZR ZM ZW
+) ) {
+ my($cust_main_county)=new FS::cust_main_county({
+ 'tax' => 0,
+ 'country' => $_,
+ });
+ my($error);
+ $error=$cust_main_county->insert;
+ die $error if $error;
+}
+
+#billing events
+foreach my $aref (
+ [ 'COMP', 'Comp invoice', '$cust_bill->comp();', 30, 'comp' ],
+ [ 'CARD', 'Batch card', '$cust_bill->batch_card();', 40, 'batch-card' ],
+ [ 'BILL', 'Send invoice', '$cust_bill->send();', 50, 'send' ],
+) {
+
+ my $part_bill_event = new FS::part_bill_event({
+ 'payby' => $aref->[0],
+ 'event' => $aref->[1],
+ 'eventcode' => $aref->[2],
+ 'seconds' => 0,
+ 'weight' => $aref->[3],
+ 'plan' => $aref->[4],
+ });
+ my($error);
+ $error=$part_bill_event->insert;
+ die $error if $error;
+
+}
+
+$dbh->commit or die $dbh->errstr;
+$dbh->disconnect or die $dbh->errstr;
+
+#print "Freeside database initialized sucessfully\n";
+
+sub usage {
+ die "Usage:\n freeside-setup [ -s ] user\n";
+}
+
+###
+# Now it becomes an object. much better.
+###
+sub tables_hash_hack {
+
+ #note that s/(date|change)/_$1/; to avoid keyword conflict.
+ #put a kludge in FS::Record to catch this or? (pry need some date-handling
+ #stuff anyway also)
+
+ my(%tables)=( #yech.}
+
+ 'agent' => {
+ 'columns' => [
+ 'agentnum', 'int', '', '',
+ 'agent', 'varchar', '', $char_d,
+ 'typenum', 'int', '', '',
+ 'freq', 'int', 'NULL', '',
+ 'prog', @perl_type,
+ ],
+ 'primary_key' => 'agentnum',
+ 'unique' => [],
+ 'index' => [ ['typenum'] ],
+ },
+
+ 'agent_type' => {
+ 'columns' => [
+ 'typenum', 'int', '', '',
+ 'atype', 'varchar', '', $char_d,
+ ],
+ 'primary_key' => 'typenum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ 'type_pkgs' => {
+ 'columns' => [
+ 'typenum', 'int', '', '',
+ 'pkgpart', 'int', '', '',
+ ],
+ 'primary_key' => '',
+ 'unique' => [ ['typenum', 'pkgpart'] ],
+ 'index' => [ ['typenum'] ],
+ },
+
+ 'cust_bill' => {
+ 'columns' => [
+ 'invnum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ '_date', @date_type,
+ 'charged', @money_type,
+ 'printed', 'int', '', '',
+ 'closed', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'invnum',
+ 'unique' => [],
+ 'index' => [ ['custnum'] ],
+ },
+
+ 'cust_bill_event' => {
+ 'columns' => [
+ 'eventnum', 'int', '', '',
+ 'invnum', 'int', '', '',
+ 'eventpart', 'int', '', '',
+ '_date', @date_type,
+ 'status', 'varchar', '', $char_d,
+ 'statustext', 'text', 'NULL', '',
+ ],
+ 'primary_key' => 'eventnum',
+ #no... there are retries now #'unique' => [ [ 'eventpart', 'invnum' ] ],
+ 'unique' => [],
+ 'index' => [ ['invnum'], ['status'] ],
+ },
+
+ 'part_bill_event' => {
+ 'columns' => [
+ 'eventpart', 'int', '', '',
+ 'payby', 'char', '', 4,
+ 'event', 'varchar', '', $char_d,
+ 'eventcode', @perl_type,
+ 'seconds', 'int', 'NULL', '',
+ 'weight', 'int', '', '',
+ 'plan', 'varchar', 'NULL', $char_d,
+ 'plandata', 'text', 'NULL', '',
+ 'disabled', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'eventpart',
+ 'unique' => [],
+ 'index' => [ ['payby'] ],
+ },
+
+ 'cust_bill_pkg' => {
+ 'columns' => [
+ 'pkgnum', 'int', '', '',
+ 'invnum', 'int', '', '',
+ 'setup', @money_type,
+ 'recur', @money_type,
+ 'sdate', @date_type,
+ 'edate', @date_type,
+ ],
+ 'primary_key' => '',
+ 'unique' => [ ['pkgnum', 'invnum'] ],
+ 'index' => [ ['invnum'] ],
+ },
+
+ 'cust_credit' => {
+ 'columns' => [
+ 'crednum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ '_date', @date_type,
+ 'amount', @money_type,
+ 'otaker', 'varchar', '', 8,
+ 'reason', 'text', 'NULL', '',
+ 'closed', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'crednum',
+ 'unique' => [],
+ 'index' => [ ['custnum'] ],
+ },
+
+ 'cust_credit_bill' => {
+ 'columns' => [
+ 'creditbillnum', 'int', '', '',
+ 'crednum', 'int', '', '',
+ 'invnum', 'int', '', '',
+ '_date', @date_type,
+ 'amount', @money_type,
+ ],
+ 'primary_key' => 'creditbillnum',
+ 'unique' => [],
+ 'index' => [ ['crednum'], ['invnum'] ],
+ },
+
+ 'cust_main' => {
+ 'columns' => [
+ 'custnum', 'int', '', '',
+ 'agentnum', 'int', '', '',
+# 'titlenum', 'int', 'NULL', '',
+ 'last', 'varchar', '', $char_d,
+# 'middle', 'varchar', 'NULL', $char_d,
+ 'first', 'varchar', '', $char_d,
+ 'ss', 'char', 'NULL', 11,
+ 'company', 'varchar', 'NULL', $char_d,
+ 'address1', 'varchar', '', $char_d,
+ 'address2', 'varchar', 'NULL', $char_d,
+ 'city', 'varchar', '', $char_d,
+ 'county', 'varchar', 'NULL', $char_d,
+ 'state', 'varchar', 'NULL', $char_d,
+ 'zip', 'varchar', '', 10,
+ 'country', 'char', '', 2,
+ 'daytime', 'varchar', 'NULL', 20,
+ 'night', 'varchar', 'NULL', 20,
+ 'fax', 'varchar', 'NULL', 12,
+ 'ship_last', 'varchar', 'NULL', $char_d,
+# 'ship_middle', 'varchar', 'NULL', $char_d,
+ 'ship_first', 'varchar', 'NULL', $char_d,
+ 'ship_company', 'varchar', 'NULL', $char_d,
+ 'ship_address1', 'varchar', 'NULL', $char_d,
+ 'ship_address2', 'varchar', 'NULL', $char_d,
+ 'ship_city', 'varchar', 'NULL', $char_d,
+ 'ship_county', 'varchar', 'NULL', $char_d,
+ 'ship_state', 'varchar', 'NULL', $char_d,
+ 'ship_zip', 'varchar', 'NULL', 10,
+ 'ship_country', 'char', 'NULL', 2,
+ 'ship_daytime', 'varchar', 'NULL', 20,
+ 'ship_night', 'varchar', 'NULL', 20,
+ 'ship_fax', 'varchar', 'NULL', 12,
+ 'payby', 'char', '', 4,
+ 'payinfo', 'varchar', 'NULL', $char_d,
+ #'paydate', @date_type,
+ 'paydate', 'varchar', 'NULL', 10,
+ 'payname', 'varchar', 'NULL', $char_d,
+ 'tax', 'char', 'NULL', 1,
+ 'otaker', 'varchar', '', 8,
+ 'refnum', 'int', '', '',
+ 'referral_custnum', 'int', 'NULL', '',
+ 'comments', 'text', 'NULL', '',
+ ],
+ 'primary_key' => 'custnum',
+ 'unique' => [],
+ #'index' => [ ['last'], ['company'] ],
+ 'index' => [ ['last'], [ 'company' ], [ 'referral_custnum' ] ],
+ },
+
+ 'cust_main_invoice' => {
+ 'columns' => [
+ 'destnum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ 'dest', 'varchar', '', $char_d,
+ ],
+ 'primary_key' => 'destnum',
+ 'unique' => [],
+ 'index' => [ ['custnum'], ],
+ },
+
+ 'cust_main_county' => { #county+state+country are checked off the
+ #cust_main_county for validation and to provide
+ # a tax rate.
+ 'columns' => [
+ 'taxnum', 'int', '', '',
+ 'state', 'varchar', 'NULL', $char_d,
+ 'county', 'varchar', 'NULL', $char_d,
+ 'country', 'char', '', 2,
+ 'taxclass', 'varchar', 'NULL', $char_d,
+ 'exempt_amount', @money_type,
+ 'tax', 'real', '', '', #tax %
+ ],
+ 'primary_key' => 'taxnum',
+ 'unique' => [],
+ # 'unique' => [ ['taxnum'], ['state', 'county'] ],
+ 'index' => [],
+ },
+
+ 'cust_pay' => {
+ 'columns' => [
+ 'paynum', 'int', '', '',
+ #now cust_bill_pay #'invnum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ 'paid', @money_type,
+ '_date', @date_type,
+ 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index into
+ # payment type table.
+ 'payinfo', 'varchar', 'NULL', 16, #see cust_main above
+ 'paybatch', 'varchar', 'NULL', $char_d, #for auditing purposes.
+ 'closed', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'paynum',
+ 'unique' => [],
+ 'index' => [ [ 'custnum' ], [ 'paybatch' ] ],
+ },
+
+ 'cust_bill_pay' => {
+ 'columns' => [
+ 'billpaynum', 'int', '', '',
+ 'invnum', 'int', '', '',
+ 'paynum', 'int', '', '',
+ 'amount', @money_type,
+ '_date', @date_type
+ ],
+ 'primary_key' => 'billpaynum',
+ 'unique' => [],
+ 'index' => [ [ 'paynum' ], [ 'invnum' ] ],
+ },
+
+ 'cust_pay_batch' => { #what's this used for again? list of customers
+ #in current CARD batch? (necessarily CARD?)
+ 'columns' => [
+ 'paybatchnum', 'int', '', '',
+ 'invnum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ 'last', 'varchar', '', $char_d,
+ 'first', 'varchar', '', $char_d,
+ 'address1', 'varchar', '', $char_d,
+ 'address2', 'varchar', 'NULL', $char_d,
+ 'city', 'varchar', '', $char_d,
+ 'state', 'varchar', 'NULL', $char_d,
+ 'zip', 'varchar', '', 10,
+ 'country', 'char', '', 2,
+# 'trancode', 'int', '', '',
+ 'cardnum', 'varchar', '', 16,
+ #'exp', @date_type,
+ 'exp', 'varchar', '', 11,
+ 'payname', 'varchar', 'NULL', $char_d,
+ 'amount', @money_type,
+ ],
+ 'primary_key' => 'paybatchnum',
+ 'unique' => [],
+ 'index' => [ ['invnum'], ['custnum'] ],
+ },
+
+ 'cust_pkg' => {
+ 'columns' => [
+ 'pkgnum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ 'pkgpart', 'int', '', '',
+ 'otaker', 'varchar', '', 8,
+ 'setup', @date_type,
+ 'bill', @date_type,
+ 'susp', @date_type,
+ 'cancel', @date_type,
+ 'expire', @date_type,
+ 'manual_flag', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'pkgnum',
+ 'unique' => [],
+ 'index' => [ ['custnum'] ],
+ },
+
+ 'cust_refund' => {
+ 'columns' => [
+ 'refundnum', 'int', '', '',
+ #now cust_credit_refund #'crednum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ '_date', @date_type,
+ 'refund', @money_type,
+ 'otaker', 'varchar', '', 8,
+ 'reason', 'varchar', '', $char_d,
+ 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index
+ # into payment type table.
+ 'payinfo', 'varchar', 'NULL', 16, #see cust_main above
+ 'paybatch', 'varchar', 'NULL', $char_d,
+ 'closed', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'refundnum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ 'cust_credit_refund' => {
+ 'columns' => [
+ 'creditrefundnum', 'int', '', '',
+ 'crednum', 'int', '', '',
+ 'refundnum', 'int', '', '',
+ 'amount', @money_type,
+ '_date', @date_type
+ ],
+ 'primary_key' => 'creditrefundnum',
+ 'unique' => [],
+ 'index' => [ [ 'crednum', 'refundnum' ] ],
+ },
+
+
+ 'cust_svc' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '',
+ 'pkgnum', 'int', 'NULL', '',
+ 'svcpart', 'int', '', '',
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [],
+ 'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'] ],
+ },
+
+ 'part_pkg' => {
+ 'columns' => [
+ 'pkgpart', 'int', '', '',
+ 'pkg', 'varchar', '', $char_d,
+ 'comment', 'varchar', '', $char_d,
+ 'setup', @perl_type,
+ 'freq', 'int', '', '', #billing frequency (months)
+ 'recur', @perl_type,
+ 'setuptax', 'char', 'NULL', 1,
+ 'recurtax', 'char', 'NULL', 1,
+ 'plan', 'varchar', 'NULL', $char_d,
+ 'plandata', 'text', 'NULL', '',
+ 'disabled', 'char', 'NULL', 1,
+ 'taxclass', 'varchar', 'NULL', $char_d,
+ ],
+ 'primary_key' => 'pkgpart',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+# 'part_title' => {
+# 'columns' => [
+# 'titlenum', 'int', '', '',
+# 'title', 'varchar', '', $char_d,
+# ],
+# 'primary_key' => 'titlenum',
+# 'unique' => [ [] ],
+# 'index' => [ [] ],
+# },
+
+ 'pkg_svc' => {
+ 'columns' => [
+ 'pkgpart', 'int', '', '',
+ 'svcpart', 'int', '', '',
+ 'quantity', 'int', '', '',
+ ],
+ 'primary_key' => '',
+ 'unique' => [ ['pkgpart', 'svcpart'] ],
+ 'index' => [ ['pkgpart'] ],
+ },
+
+ 'part_referral' => {
+ 'columns' => [
+ 'refnum', 'int', '', '',
+ 'referral', 'varchar', '', $char_d,
+ ],
+ 'primary_key' => 'refnum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ 'part_svc' => {
+ 'columns' => [
+ 'svcpart', 'int', '', '',
+ 'svc', 'varchar', '', $char_d,
+ 'svcdb', 'varchar', '', $char_d,
+ 'disabled', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'svcpart',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ 'part_svc_column' => {
+ 'columns' => [
+ 'columnnum', 'int', '', '',
+ 'svcpart', 'int', '', '',
+ 'columnname', 'varchar', '', 64,
+ 'columnvalue', 'varchar', 'NULL', $char_d,
+ 'columnflag', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'columnnum',
+ 'unique' => [ [ 'svcpart', 'columnname' ] ],
+ 'index' => [ [ 'svcpart' ] ],
+ },
+
+ #(this should be renamed to part_pop)
+ 'svc_acct_pop' => {
+ 'columns' => [
+ 'popnum', 'int', '', '',
+ 'city', 'varchar', '', $char_d,
+ 'state', 'varchar', '', $char_d,
+ 'ac', 'char', '', 3,
+ 'exch', 'char', '', 3,
+ 'loc', 'char', 'NULL', 4, #NULL for legacy purposes
+ ],
+ 'primary_key' => 'popnum',
+ 'unique' => [],
+ 'index' => [ [ 'state' ] ],
+ },
+
+ 'part_pop_local' => {
+ 'columns' => [
+ 'localnum', 'int', '', '',
+ 'popnum', 'int', '', '',
+ 'city', 'varchar', 'NULL', $char_d,
+ 'state', 'char', 'NULL', 2,
+ 'npa', 'char', '', 3,
+ 'nxx', 'char', '', 3,
+ ],
+ 'primary_key' => 'localnum',
+ 'unique' => [],
+ 'index' => [ [ 'npa', 'nxx' ], [ 'popnum' ] ],
+ },
+
+ 'svc_acct' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '',
+ 'username', 'varchar', '', $username_len, #unique (& remove dup code)
+ '_password', 'varchar', '', 50, #13 for encryped pw's plus ' *SUSPENDED* (mp5 passwords can be 34)
+ 'sec_phrase', 'varchar', 'NULL', $char_d,
+ 'popnum', 'int', 'NULL', '',
+ 'uid', 'int', 'NULL', '',
+ 'gid', 'int', 'NULL', '',
+ 'finger', 'varchar', 'NULL', $char_d,
+ 'dir', 'varchar', 'NULL', $char_d,
+ 'shell', 'varchar', 'NULL', $char_d,
+ 'quota', 'varchar', 'NULL', $char_d,
+ 'slipip', 'varchar', 'NULL', 15, #four TINYINTs, bah.
+ 'seconds', 'int', 'NULL', '', #uhhhh
+ 'domsvc', 'int', '', '',
+ ],
+ 'primary_key' => 'svcnum',
+ #'unique' => [ [ 'username', 'domsvc' ] ],
+ 'unique' => [],
+ 'index' => [ ['username'], ['domsvc'] ],
+ },
+
+# 'svc_acct_sm' => {
+# 'columns' => [
+# 'svcnum', 'int', '', '',
+# 'domsvc', 'int', '', '',
+# 'domuid', 'int', '', '',
+# 'domuser', 'varchar', '', $char_d,
+# ],
+# 'primary_key' => 'svcnum',
+# 'unique' => [ [] ],
+# 'index' => [ ['domsvc'], ['domuid'] ],
+# },
+
+ #'svc_charge' => {
+ # 'columns' => [
+ # 'svcnum', 'int', '', '',
+ # 'amount', @money_type,
+ # ],
+ # 'primary_key' => 'svcnum',
+ # 'unique' => [ [] ],
+ # 'index' => [ [] ],
+ #},
+
+ 'svc_domain' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '',
+ 'domain', 'varchar', '', $char_d,
+ 'catchall', 'int', 'NULL', '',
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [ ['domain'] ],
+ 'index' => [],
+ },
+
+ 'domain_record' => {
+ 'columns' => [
+ 'recnum', 'int', '', '',
+ 'svcnum', 'int', '', '',
+ 'reczone', 'varchar', '', $char_d,
+ 'recaf', 'char', '', 2,
+ 'rectype', 'char', '', 5,
+ 'recdata', 'varchar', '', $char_d,
+ ],
+ 'primary_key' => 'recnum',
+ 'unique' => [],
+ 'index' => [ ['svcnum'] ],
+ },
+
+ 'svc_forward' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '',
+ 'srcsvc', 'int', '', '',
+ 'dstsvc', 'int', '', '',
+ 'dst', 'varchar', 'NULL', $char_d,
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [],
+ 'index' => [ ['srcsvc'], ['dstsvc'] ],
+ },
+
+ 'svc_www' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '',
+ 'recnum', 'int', '', '',
+ 'usersvc', 'int', '', '',
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ #'svc_wo' => {
+ # 'columns' => [
+ # 'svcnum', 'int', '', '',
+ # 'svcnum', 'int', '', '',
+ # 'svcnum', 'int', '', '',
+ # 'worker', 'varchar', '', $char_d,
+ # '_date', @date_type,
+ # ],
+ # 'primary_key' => 'svcnum',
+ # 'unique' => [ [] ],
+ # 'index' => [ [] ],
+ #},
+
+ 'prepay_credit' => {
+ 'columns' => [
+ 'prepaynum', 'int', '', '',
+ 'identifier', 'varchar', '', $char_d,
+ 'amount', @money_type,
+ 'seconds', 'int', 'NULL', '',
+ ],
+ 'primary_key' => 'prepaynum',
+ 'unique' => [ ['identifier'] ],
+ 'index' => [],
+ },
+
+ 'port' => {
+ 'columns' => [
+ 'portnum', 'int', '', '',
+ 'ip', 'varchar', 'NULL', 15,
+ 'nasport', 'int', 'NULL', '',
+ 'nasnum', 'int', '', '',
+ ],
+ 'primary_key' => 'portnum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ 'nas' => {
+ 'columns' => [
+ 'nasnum', 'int', '', '',
+ 'nas', 'varchar', '', $char_d,
+ 'nasip', 'varchar', '', 15,
+ 'nasfqdn', 'varchar', '', $char_d,
+ 'last', 'int', '', '',
+ ],
+ 'primary_key' => 'nasnum',
+ 'unique' => [ [ 'nas' ], [ 'nasip' ] ],
+ 'index' => [ [ 'last' ] ],
+ },
+
+ 'session' => {
+ 'columns' => [
+ 'sessionnum', 'int', '', '',
+ 'portnum', 'int', '', '',
+ 'svcnum', 'int', '', '',
+ 'login', @date_type,
+ 'logout', @date_type,
+ ],
+ 'primary_key' => 'sessionnum',
+ 'unique' => [],
+ 'index' => [ [ 'portnum' ] ],
+ },
+
+ 'queue' => {
+ 'columns' => [
+ 'jobnum', 'int', '', '',
+ 'job', 'text', '', '',
+ '_date', 'int', '', '',
+ 'status', 'varchar', '', $char_d,
+ 'statustext', 'text', 'NULL', '',
+ 'svcnum', 'int', 'NULL', '',
+ ],
+ 'primary_key' => 'jobnum',
+ 'unique' => [],
+ 'index' => [ [ 'svcnum' ], [ 'status' ] ],
+ },
+
+ 'queue_arg' => {
+ 'columns' => [
+ 'argnum', 'int', '', '',
+ 'jobnum', 'int', '', '',
+ 'arg', 'text', 'NULL', '',
+ ],
+ 'primary_key' => 'argnum',
+ 'unique' => [],
+ 'index' => [ [ 'jobnum' ] ],
+ },
+
+ 'queue_depend' => {
+ 'columns' => [
+ 'dependnum', 'int', '', '',
+ 'jobnum', 'int', '', '',
+ 'depend_jobnum', 'int', '', '',
+ ],
+ 'primary_key' => 'dependnum',
+ 'unique' => [],
+ 'index' => [ [ 'jobnum' ], [ 'depend_jobnum' ] ],
+ },
+
+ 'export_svc' => {
+ 'columns' => [
+ 'exportsvcnum' => 'int', '', '',
+ 'exportnum' => 'int', '', '',
+ 'svcpart' => 'int', '', '',
+ ],
+ 'primary_key' => 'exportsvcnum',
+ 'unique' => [ [ 'exportnum', 'svcpart' ] ],
+ 'index' => [ [ 'exportnum' ], [ 'svcpart' ] ],
+ },
+
+ 'part_export' => {
+ 'columns' => [
+ 'exportnum', 'int', '', '',
+ #'svcpart', 'int', '', '',
+ 'machine', 'varchar', '', $char_d,
+ 'exporttype', 'varchar', '', $char_d,
+ 'nodomain', 'char', 'NULL', 1,
+ ],
+ 'primary_key' => 'exportnum',
+ 'unique' => [],
+ 'index' => [ [ 'machine' ], [ 'exporttype' ] ],
+ },
+
+ 'part_export_option' => {
+ 'columns' => [
+ 'optionnum', 'int', '', '',
+ 'exportnum', 'int', '', '',
+ 'optionname', 'varchar', '', $char_d,
+ 'optionvalue', 'text', 'NULL', '',
+ ],
+ 'primary_key' => 'optionnum',
+ 'unique' => [],
+ 'index' => [ [ 'exportnum' ], [ 'optionname' ] ],
+ },
+
+ 'radius_usergroup' => {
+ 'columns' => [
+ 'usergroupnum', 'int', '', '',
+ 'svcnum', 'int', '', '',
+ 'groupname', 'varchar', '', $char_d,
+ ],
+ 'primary_key' => 'usergroupnum',
+ 'unique' => [],
+ 'index' => [ [ 'svcnum' ], [ 'groupname' ] ],
+ },
+
+ 'msgcat' => {
+ 'columns' => [
+ 'msgnum', 'int', '', '',
+ 'msgcode', 'varchar', '', $char_d,
+ 'locale', 'varchar', '', 16,
+ 'msg', 'text', '', '',
+ ],
+ 'primary_key' => 'msgnum',
+ 'unique' => [ [ 'msgcode', 'locale' ] ],
+ 'index' => [],
+ },
+
+ 'cust_tax_exempt' => {
+ 'columns' => [
+ 'exemptnum', 'int', '', '',
+ 'custnum', 'int', '', '',
+ 'taxnum', 'int', '', '',
+ 'year', 'int', '', '',
+ 'month', 'int', '', '',
+ 'amount', @money_type,
+ ],
+ 'primary_key' => 'exemptnum',
+ 'unique' => [ [ 'custnum', 'taxnum', 'year', 'month' ] ],
+ 'index' => [],
+ },
+
+
+
+ );
+
+ %tables;
+
+}
+
diff --git a/FS/bin/freeside-sqlradius-reset b/FS/bin/freeside-sqlradius-reset
index 41f3358f6..9d3a6a700 100755
--- a/FS/bin/freeside-sqlradius-reset
+++ b/FS/bin/freeside-sqlradius-reset
@@ -46,7 +46,7 @@ foreach my $export ( @exports ) {
sub usage {
#die "Usage:\n\n sqlradius_reset user machine\n";
- die "Usage:\n\n sqlradius_reset user\n";
+ die "Usage:\n\n freeside-sqlradius-reset user\n";
}
=head1 NAME
@@ -66,7 +66,7 @@ B<username> is a username added by freeside-adduser.
=head1 SEE ALSO
-<FS::part_export>, L<FS::part_export::sqlradius>
+L<freeside-reexport>, L<FS::part_export>, L<FS::part_export::sqlradius>
=cut
diff --git a/FS/t/ClientAPI.t b/FS/t/ClientAPI.t
new file mode 100644
index 000000000..973d8dada
--- /dev/null
+++ b/FS/t/ClientAPI.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::ClientAPI;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/InitHandler.t b/FS/t/InitHandler.t
new file mode 100644
index 000000000..0ce60c833
--- /dev/null
+++ b/FS/t/InitHandler.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::InitHandler;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-bind.t b/FS/t/part_export-bind.t
new file mode 100644
index 000000000..d0c96be40
--- /dev/null
+++ b/FS/t/part_export-bind.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::bind;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-bind_slave.t b/FS/t/part_export-bind_slave.t
new file mode 100644
index 000000000..c6a038610
--- /dev/null
+++ b/FS/t/part_export-bind_slave.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::bind_slave;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-domain_shellcommands.t b/FS/t/part_export-domain_shellcommands.t
new file mode 100644
index 000000000..a2a44fbfb
--- /dev/null
+++ b/FS/t/part_export-domain_shellcommands.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::domain_shellcommands;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-http.t b/FS/t/part_export-http.t
new file mode 100644
index 000000000..ea41b939f
--- /dev/null
+++ b/FS/t/part_export-http.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::http;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-null.t b/FS/t/part_export-null.t
new file mode 100644
index 000000000..055cdcee6
--- /dev/null
+++ b/FS/t/part_export-null.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::null;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-shellcommands_withdomain.t b/FS/t/part_export-shellcommands_withdomain.t
new file mode 100644
index 000000000..c0bd1bbb0
--- /dev/null
+++ b/FS/t/part_export-shellcommands_withdomain.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::shellcommands_withdomain;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-sqlmail.t b/FS/t/part_export-sqlmail.t
new file mode 100644
index 000000000..b048a75a5
--- /dev/null
+++ b/FS/t/part_export-sqlmail.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::sqlmail;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-sysvshell.t b/FS/t/part_export-sysvshell.t
new file mode 100644
index 000000000..7fc24acb1
--- /dev/null
+++ b/FS/t/part_export-sysvshell.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::sysvshell;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-www_shellcommands.t b/FS/t/part_export-www_shellcommands.t
new file mode 100644
index 000000000..2ea79cf97
--- /dev/null
+++ b/FS/t/part_export-www_shellcommands.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::www_shellcommands;
+$loaded=1;
+print "ok 1\n";