use Exporter;
use Carp qw(carp cluck croak confess);
use Scalar::Util qw( blessed );
+use File::Slurp qw( slurp );
use File::CounterFile;
-use Locale::Country;
use Text::CSV_XS;
-use File::Slurp qw( slurp );
use DBI qw(:sql_types);
use DBIx::DBSchema 0.38;
-use FS::UID qw(dbh getotaker datasrc driver_name);
+use Locale::Country;
+use Locale::Currency;
+use NetAddr::IP; # for validation
+use FS::UID qw(dbh datasrc driver_name);
use FS::CurrentUser;
use FS::Schema qw(dbdef);
use FS::SearchCache;
use FS::Msgcat qw(gettext);
-use NetAddr::IP; # for validation
-use Data::Dumper;
#use FS::Conf; #dependency loop bs, in install_callback below instead
use FS::part_virtual_field;
# grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
# ) or croak "Error executing \"$statement\": ". $sth->errstr;
- $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
+ my $ok = $sth->execute;
+ if (!$ok) {
+ my $error = "Error executing \"$statement\"";
+ $error .= ' (' . join(', ', map {"'$_'"} @value) . ')' if @value;
+ $error .= ': '. $sth->errstr;
+ croak $error;
+ }
my $table = $stable[0];
my $pkey = '';
# Check for encrypted fields and decrypt them.
## only in the local copy, not the cached object
if ( $conf_encryption
- && eval 'defined(@FS::'. $table . '::encrypted_fields)' ) {
+ && eval '@FS::'. $table . '::encrypted_fields' ) {
foreach my $record (@return) {
foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
next if $field eq 'payinfo'
return @return;
}
+=item _query
+
+Construct the SQL statement and parameter-binding list for qsearch. Takes
+the qsearch parameters.
+
+Returns a hash containing:
+'table': The primary table name (if there is one).
+'statement': The SQL statement itself.
+'bind_type': An arrayref of bind types.
+'value': An arrayref of parameter values.
+'cache': The cache object, if one was passed.
+
+=cut
+
+sub _query {
+ my( @stable, @record, @cache );
+ my( @select, @extra_sql, @extra_param, @order_by, @addl_from );
+ my @debug = ();
+ my $cursor = '';
+ my %union_options = ();
+ if ( ref($_[0]) eq 'ARRAY' ) {
+ my $optlist = shift;
+ %union_options = @_;
+ foreach my $href ( @$optlist ) {
+ push @stable, ( $href->{'table'} or die "table name is required" );
+ push @record, ( $href->{'hashref'} || {} );
+ push @select, ( $href->{'select'} || '*' );
+ push @extra_sql, ( $href->{'extra_sql'} || '' );
+ push @extra_param, ( $href->{'extra_param'} || [] );
+ push @order_by, ( $href->{'order_by'} || '' );
+ push @cache, ( $href->{'cache_obj'} || '' );
+ push @addl_from, ( $href->{'addl_from'} || '' );
+ push @debug, ( $href->{'debug'} || '' );
+ }
+ die "at least one hashref is required" unless scalar(@stable);
+ } elsif ( ref($_[0]) eq 'HASH' ) {
+ my $opt = shift;
+ $stable[0] = $opt->{'table'} or die "table name is required";
+ $record[0] = $opt->{'hashref'} || {};
+ $select[0] = $opt->{'select'} || '*';
+ $extra_sql[0] = $opt->{'extra_sql'} || '';
+ $extra_param[0] = $opt->{'extra_param'} || [];
+ $order_by[0] = $opt->{'order_by'} || '';
+ $cache[0] = $opt->{'cache_obj'} || '';
+ $addl_from[0] = $opt->{'addl_from'} || '';
+ $debug[0] = $opt->{'debug'} || '';
+ } else {
+ ( $stable[0],
+ $record[0],
+ $select[0],
+ $extra_sql[0],
+ $cache[0],
+ $addl_from[0]
+ ) = @_;
+ $select[0] ||= '*';
+ }
+ my $cache = $cache[0];
+
+ my @statement = ();
+ my @value = ();
+ my @bind_type = ();
+
+ my $result_table = $stable[0];
+ foreach my $stable ( @stable ) {
+ #stop altering the caller's hashref
+ my $record = { %{ shift(@record) || {} } };#and be liberal in receipt
+ my $select = shift @select;
+ my $extra_sql = shift @extra_sql;
+ my $extra_param = shift @extra_param;
+ my $order_by = shift @order_by;
+ my $cache = shift @cache;
+ my $addl_from = shift @addl_from;
+ my $debug = shift @debug;
+
+ #$stable =~ /^([\w\_]+)$/ or die "Illegal table: $table";
+ #for jsearch
+ $stable =~ /^([\w\s\(\)\.\,\=]+)$/ or die "Illegal table: $stable";
+ $stable = $1;
+
+ $result_table = '' if $result_table ne $stable;
+
+ my $table = $cache ? $cache->table : $stable;
+ my $dbdef_table = dbdef->table($table)
+ or die "No schema for table $table found - ".
+ "do you need to run freeside-upgrade?";
+ my $pkey = $dbdef_table->primary_key;
+
+ my @real_fields = grep exists($record->{$_}), real_fields($table);
+
+ my $statement .= "SELECT $select FROM $stable";
+ $statement .= " $addl_from" if $addl_from;
+ if ( @real_fields ) {
+ $statement .= ' WHERE '. join(' AND ',
+ get_real_fields($table, $record, \@real_fields));
+ }
+
+ $statement .= " $extra_sql" if defined($extra_sql);
+ $statement .= " $order_by" if defined($order_by);
+
+ push @statement, $statement;
+
+ warn "[debug]$me $statement\n" if $DEBUG > 1 || $debug;
+
+
+ foreach my $field (
+ grep defined( $record->{$_} ) && $record->{$_} ne '', @real_fields
+ ) {
+
+ my $value = $record->{$field};
+ my $op = (ref($value) && $value->{op}) ? $value->{op} : '=';
+ $value = $value->{'value'} if ref($value);
+ my $type = dbdef->table($table)->column($field)->type;
+
+ my $bind_type = _bind_type($type, $value);
+
+ #if ( $DEBUG > 2 ) {
+ # no strict 'refs';
+ # %TYPE = map { &{"DBI::$_"}() => $_ } @{ $DBI::EXPORT_TAGS{sql_types} }
+ # unless keys %TYPE;
+ # warn " bind_param $bind (for field $field), $value, TYPE $TYPE{$TYPE}\n";
+ #}
+
+ push @value, $value;
+ push @bind_type, $bind_type;
+
+ }
+
+ foreach my $param ( @$extra_param ) {
+ my $bind_type = { TYPE => SQL_VARCHAR };
+ my $value = $param;
+ if ( ref($param) ) {
+ $value = $param->[0];
+ my $type = $param->[1];
+ $bind_type = _bind_type($type, $value);
+ }
+ push @value, $value;
+ push @bind_type, $bind_type;
+ }
+ }
+
+ my $statement = join( ' ) UNION ( ', @statement );
+ $statement = "( $statement )" if scalar(@statement) > 1;
+ $statement .= " $union_options{order_by}" if $union_options{order_by};
+
+ return {
+ statement => $statement,
+ bind_type => \@bind_type,
+ value => \@value,
+ table => $result_table,
+ cache => $cache,
+ };
+}
+
+# qsearch should eventually use this
+sub _from_hashref {
+ my ($table, $cache, @hashrefs) = @_;
+ my @return;
+ # XXX get rid of these string evals at some point
+ # (when we have time to test it)
+ # my $class = "FS::$table" if $table;
+ # if ( $class and $class->isa('FS::Record') )
+ # if ( $class->can('new') eq \&new )
+ #
+ if ( $table && eval 'scalar(@FS::'. $table. '::ISA);' ) {
+ if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
+ #derivied class didn't override new method, so this optimization is safe
+ if ( $cache ) {
+ @return = map {
+ new_or_cached( "FS::$table", { %{$_} }, $cache )
+ } @hashrefs;
+ } else {
+ @return = map {
+ new( "FS::$table", { %{$_} } )
+ } @hashrefs;
+ }
+ } else {
+ #okay, its been tested
+ # warn "untested code (class FS::$table uses custom new method)";
+ @return = map {
+ eval 'FS::'. $table. '->new( { %{$_} } )';
+ } @hashrefs;
+ }
+
+ # Check for encrypted fields and decrypt them.
+ ## only in the local copy, not the cached object
+ if ( $conf_encryption
+ && eval '@FS::'. $table . '::encrypted_fields' ) {
+ foreach my $record (@return) {
+ foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
+ next if $field eq 'payinfo'
+ && ($record->isa('FS::payinfo_transaction_Mixin')
+ || $record->isa('FS::payinfo_Mixin') )
+ && $record->payby
+ && !grep { $record->payby eq $_ } @encrypt_payby;
+ # Set it directly... This may cause a problem in the future...
+ $record->setfield($field, $record->decrypt($record->getfield($field)));
+ }
+ }
+ }
+ } else {
+ cluck "warning: FS::$table not loaded; returning FS::Record objects"
+ unless $nowarn_classload;
+ @return = map {
+ FS::Record->new( $table, { %{$_} } );
+ } @hashrefs;
+ }
+ return @return;
+}
+
## makes this easier to read
sub get_real_fields {
format_sep_chars => $opt->{format_sep_chars},
format_fixedlength_formats => $opt->{format_fixedlength_formats},
format_xml_formats => $opt->{format_xml_formats},
+ format_asn_formats => $opt->{format_asn_formats},
format_row_callbacks => $opt->{format_row_callbacks},
#per-import
job => $job,
=cut
+use Data::Dumper;
sub batch_import {
my $param = shift;
my $file = $param->{file};
my $params = $param->{params} || {};
- my( $type, $header, $sep_char, $fixedlength_format,
- $xml_format, $row_callback, @fields );
+ my( $type, $header, $sep_char,
+ $fixedlength_format, $xml_format, $asn_format,
+ $row_callback, @fields );
my $postinsert_callback = '';
$postinsert_callback = $param->{'postinsert_callback'}
? $param->{'format_xml_formats'}{ $param->{'format'} }
: '';
+ $asn_format =
+ $param->{'format_asn_formats'}
+ ? $param->{'format_asn_formats'}{ $param->{'format'} }
+ : '';
+
$row_callback =
$param->{'format_row_callbacks'}
? $param->{'format_row_callbacks'}{ $param->{'format'} }
my $count;
my $parser;
my @buffer = ();
+ my $asn_header_buffer;
if ( $type eq 'csv' || $type eq 'fixedlength' ) {
if ( $type eq 'csv' ) {
- my %attr = ();
+ my %attr = ( 'binary' => 1, );
$attr{sep_char} = $sep_char if $sep_char;
$parser = new Text::CSV_XS \%attr;
$count++;
$row = $header || 0;
+
} elsif ( $type eq 'xml' ) {
+
# FS::pay_batch
eval "use XML::Simple;";
die $@ if $@;
$rows = $rows->{$_} foreach @$xmlrow;
$rows = [ $rows ] if ref($rows) ne 'ARRAY';
$count = @buffer = @$rows;
+
+ } elsif ( $type eq 'asn.1' ) {
+
+ eval "use Convert::ASN1";
+ die $@ if $@;
+
+ my $asn = Convert::ASN1->new;
+ $asn->prepare( $asn_format->{'spec'} ) or die $asn->error;
+
+ $parser = $asn->find( $asn_format->{'macro'} ) or die $asn->error;
+
+ my $data = slurp($file);
+ my $asn_output = $parser->decode( $data )
+ or return "No ". $asn_format->{'macro'}. " found\n";
+
+ $asn_header_buffer = &{ $asn_format->{'header_buffer'} }( $asn_output );
+
+ my $rows = &{ $asn_format->{'arrayref'} }( $asn_output );
+ $count = @buffer = @$rows;
+
} else {
die "Unknown file type $type\n";
}
while (1) {
my @columns = ();
+ my %hash = %$params;
if ( $type eq 'csv' ) {
last unless scalar(@buffer);
#warn $z++. ": $_\n" for @columns;
} elsif ( $type eq 'xml' ) {
+
# $parser = [ 'Column0Key', 'Column1Key' ... ]
last unless scalar(@buffer);
my $row = shift @buffer;
@columns = @{ $row }{ @$parser };
+
+ } elsif ( $type eq 'asn.1' ) {
+
+ last unless scalar(@buffer);
+ my $row = shift @buffer;
+ &{ $asn_format->{row_callback} }( $row, $asn_header_buffer )
+ if $asn_format->{row_callback};
+ foreach my $key ( keys %{ $asn_format->{map} } ) {
+ $hash{$key} = &{ $asn_format->{map}{$key} }( $row, $asn_header_buffer );
+ }
+
} else {
die "Unknown file type $type\n";
}
my @later = ();
- my %hash = %$params;
foreach my $field ( @fields ) {
return "Empty file!";
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
''; #no error
my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields;
"INSERT INTO h_". $self->table. " ( ".
- join(', ', qw(history_date history_user history_action), @fields ).
+ join(', ', qw(history_date history_usernum history_action), @fields ).
") VALUES (".
- join(', ', $time, dbh->quote(getotaker()), dbh->quote($action), @values).
+ join(', ', $time,
+ $FS::CurrentUser::CurrentUser->usernum,
+ dbh->quote($action),
+ @values
+ ).
")"
;
}
#warn "field $field is tainted" if is_tainted($field);
my($counter) = new File::CounterFile "$table.$field",0;
-# hack for web demo
-# getotaker() =~ /^([\w\-]{1,16})$/ or die "Illegal CGI REMOTE_USER!";
-# my($user)=$1;
-# my($counter) = new File::CounterFile "$user/$table.$field",0;
-# endhack
my $index = $counter->inc;
$index = $counter->inc while qsearchs($table, { $field=>$index } );
sub ut_money {
my($self,$field)=@_;
- $self->setfield($field, 0) if $self->getfield($field) eq '';
- $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/
- or return "Illegal (money) $field: ". $self->getfield($field);
- #$self->setfield($field, "$1$2$3" || 0);
- $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0);
+
+ if ( $self->getfield($field) eq '' ) {
+ $self->setfield($field, 0);
+ } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{1})\s*$/ ) {
+ #handle one decimal place without barfing out
+ $self->setfield($field, ( ($1||''). ($2||''). ($3.'0') ) || 0);
+ } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/ ) {
+ $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0);
+ } else {
+ return "Illegal (money) $field: ". $self->getfield($field);
+ }
+
'';
}
$self->ut_money($field);
}
+=item ut_currencyn COLUMN
+
+Check/untaint currency indicators, such as USD or EUR. May be null. If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_currencyn {
+ my($self, $field) = @_;
+ if ($self->getfield($field) eq '') { #can be null
+ $self->setfield($field, '');
+ return '';
+ }
+ $self->ut_currency($field);
+}
+
+=item ut_currency COLUMN
+
+Check/untaint currency indicators, such as USD or EUR. May not be null. If
+there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_currency {
+ my($self, $field) = @_;
+ my $value = uc( $self->getfield($field) );
+ if ( code2currency($value) ) {
+ $self->setfield($value);
+ } else {
+ return "Unknown currency $value";
+ }
+
+ '';
+}
+
=item ut_text COLUMN
Check/untaint text. Alphanumerics, spaces, and the following punctuation
}
-
=item ut_domain COLUMN
-Check/untaint host and domain names.
+Check/untaint host and domain names. May not be null.
=cut
my( $self, $field ) = @_;
#$self->getfield($field) =~/^(\w+\.)*\w+$/
$self->getfield($field) =~/^(([\w\-]+\.)*\w+)$/
- or return "Illegal (domain) $field: ". $self->getfield($field);
+ or return "Illegal (hostname) $field: ". $self->getfield($field);
$self->setfield($field,$1);
'';
}
+=item ut_domainn COLUMN
+
+Check/untaint host and domain names. May be null.
+
+=cut
+
+sub ut_domainn {
+ my( $self, $field ) = @_;
+ if ( $self->getfield($field) =~ /^()$/ ) {
+ $self->setfield($field,'');
+ '';
+ } else {
+ $self->ut_domain($field);
+ }
+}
+
=item ut_name COLUMN
Check/untaint proper names; allows alphanumerics, spaces and the following
# warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n";
$self->getfield($field) =~ /^([\w \,\.\-\']+)$/
or return gettext('illegal_name'). " $field: ". $self->getfield($field);
- $self->setfield($field,$1);
+ my $name = $1;
+ $name =~ s/^\s+//;
+ $name =~ s/\s+$//;
+ $name =~ s/\s+/ /g;
+ $self->setfield($field, $name);
'';
}
+=item ut_namen COLUMN
+
+Check/untaint proper names; allows alphanumerics, spaces and the following
+punctuation: , . - '
+
+May not be null.
+
+=cut
+
+sub ut_namen {
+ my( $self, $field ) = @_;
+ return $self->setfield($field, '') if $self->getfield($field) =~ /^$/;
+ $self->ut_name($field);
+}
+
=item ut_zip COLUMN
Check/untaint zip codes.
: '';
}
+=item ut_flag COLUMN
+
+Check/untaint a column if it contains either an empty string or 'Y'. This
+is the standard form for boolean flags in Freeside.
+
+=cut
+
+sub ut_flag {
+ my( $self, $field ) = @_;
+ my $value = uc($self->getfield($field));
+ if ( $value eq '' or $value eq 'Y' ) {
+ $self->setfield($field, $value);
+ return '';
+ }
+ return "Illegal (flag) field $field: $value";
+}
=item ut_foreign_key COLUMN FOREIGN_TABLE FOREIGN_COLUMN