use base qw( Exporter );
use strict;
+use charnames ':full';
use vars qw( $AUTOLOAD
- %virtual_fields_cache %fk_method_cache
+ %virtual_fields_cache %fk_method_cache $fk_table_cache
$money_char $lat_lower $lon_upper
+ $use_placeholders
);
use Carp qw(carp cluck croak confess);
use Scalar::Util qw( blessed );
dbh fields hfields qsearch qsearchs dbdef jsearch
str2time_sql str2time_sql_closing regexp_sql not_regexp_sql
concat_sql group_concat_sql
- midnight_sql
+ midnight_sql fk_methods_init
);
our $DEBUG = 0;
our $me = '[FS::Record]';
+$use_placeholders = 0;
+
our $nowarn_identical = 0;
our $nowarn_classload = 0;
our $no_update_diff = 0;
our $no_history = 0;
+our $qsearch_qualify_columns = 1;
+
our $no_check_foreign = 1; #well, not inefficiently in perl by default anymore
my $rsa_module;
our $conf = '';
our $conf_encryption = '';
+our $conf_encryptionmodule = '';
+our $conf_encryptionpublickey = '';
+our $conf_encryptionprivatekey = '';
FS::UID->install_callback( sub {
eval "use FS::Conf;";
die $@ if $@;
$conf = FS::Conf->new;
- $conf_encryption = $conf->exists('encryption');
+ $conf_encryption = $conf->exists('encryption');
+ $conf_encryptionmodule = $conf->config('encryptionmodule');
+ $conf_encryptionpublickey = join("\n",$conf->config('encryptionpublickey'));
+ $conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
$money_char = $conf->config('money_char') || '$';
my $nw_coords = $conf->exists('geocode-require_nw_coordinates');
$lat_lower = $nw_coords ? 1 : -90;
eval "sub PG_BYTEA { die 'guru meditation #9: calling PG_BYTEA when not running Pg?'; }";
}
- foreach my $table ( dbdef->tables ) {
- $fk_method_cache{$table} = fk_methods($table);
- }
+ #fk_methods_init();
} );
$self->{'modified'} = 0;
+ $self->_simplecache($self->{'Hash'}) if $self->can('_simplecache');
$self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
$self;
my @real_fields = grep exists($record->{$_}), real_fields($table);
my $statement .= "SELECT $select FROM $stable";
- $statement .= " $addl_from" if $addl_from;
+ my $alias_main;
+ if ( $addl_from ) {
+ $statement .= " $addl_from";
+ # detect aliasing of the main table
+ if ( $addl_from =~ /^\s*AS\s+(\w+)/i ) {
+ $alias_main = $1;
+ }
+ }
if ( @real_fields ) {
$statement .= ' WHERE '. join(' AND ',
- get_real_fields($table, $record, \@real_fields));
+ get_real_fields($table, $record, \@real_fields, $alias_main));
}
$statement .= " $extra_sql" if defined($extra_sql);
$sth->finish;
+ #below was refactored out to _from_hashref, this should use it at some point
+
my @return;
if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
# Check for encrypted fields and decrypt them.
## only in the local copy, not the cached object
+ no warnings 'deprecated'; # XXX silence the warning for now
if ( $conf_encryption
&& eval '@FS::'. $table . '::encrypted_fields' ) {
foreach my $record (@return) {
return @return;
}
-## makes this easier to read
-
sub get_real_fields {
my $table = shift;
my $record = shift;
my $real_fields = shift;
+ my $alias_main = shift; # defaults to undef
+ $alias_main ||= $table;
- ## this huge map was previously inline, just broke it out to help read the qsearch method, should be optimized for readability
- return (
- map {
+ ## could be optimized more for readability
+ return (
+ map {
my $op = '=';
my $column = $_;
+ my $table_column = $qsearch_qualify_columns ? "$alias_main.$column" : $column;
my $type = dbdef->table($table)->column($column)->type;
my $value = $record->{$column};
$value = $value->{'value'} if ref($value);
- if ( ref($record->{$_}) ) {
- $op = $record->{$_}{'op'} if $record->{$_}{'op'};
+
+ if ( ref($record->{$column}) ) {
+ $op = $record->{$column}{'op'} if $record->{$column}{'op'};
#$op = 'LIKE' if $op =~ /^ILIKE$/i && driver_name ne 'Pg';
if ( uc($op) eq 'ILIKE' ) {
$op = 'LIKE';
- $record->{$_}{'value'} = lc($record->{$_}{'value'});
- $column = "LOWER($_)";
+ $record->{$column}{'value'} = lc($record->{$column}{'value'});
+ $table_column = "LOWER($table_column)";
}
- $record->{$_} = $record->{$_}{'value'}
+ $record->{$column} = $record->{$column}{'value'}
}
- if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
+ if ( ! defined( $record->{$column} ) || $record->{$column} eq '' ) {
if ( $op eq '=' ) {
if ( driver_name eq 'Pg' ) {
if ( $type =~ /(int|numeric|real|float4|(big)?serial)/i ) {
- qq-( $column IS NULL )-;
+ qq-( $table_column IS NULL )-;
} else {
- qq-( $column IS NULL OR $column = '' )-;
+ qq-( $table_column IS NULL OR $table_column = '' )-;
}
} else {
- qq-( $column IS NULL OR $column = "" )-;
+ qq-( $table_column IS NULL OR $table_column = "" )-;
}
} elsif ( $op eq '!=' ) {
if ( driver_name eq 'Pg' ) {
if ( $type =~ /(int|numeric|real|float4|(big)?serial)/i ) {
- qq-( $column IS NOT NULL )-;
+ qq-( $table_column IS NOT NULL )-;
} else {
- qq-( $column IS NOT NULL AND $column != '' )-;
+ qq-( $table_column IS NOT NULL AND $table_column != '' )-;
}
} else {
- qq-( $column IS NOT NULL AND $column != "" )-;
+ qq-( $table_column IS NOT NULL AND $table_column != "" )-;
}
} else {
if ( driver_name eq 'Pg' ) {
- qq-( $column $op '' )-;
+ qq-( $table_column $op '' )-;
} else {
- qq-( $column $op "" )-;
+ qq-( $table_column $op "" )-;
}
}
} elsif ( $op eq '!=' ) {
- qq-( $column IS NULL OR $column != ? )-;
+ qq-( $table_column IS NULL OR $table_column != ? )-;
#if this needs to be re-enabled, it needs to use a custom op like
#"APPROX=" or something (better name?, not '=', to avoid affecting other
# searches
#} elsif ( $op eq 'APPROX=' && _is_fs_float( $type, $value ) ) {
- # ( "$column <= ?", "$column >= ?" );
+ # ( "$table_column <= ?", "$table_column >= ?" );
} else {
- "$column $op ?";
+ "$table_column $op ?";
}
- } @{ $real_fields } );
+
+ } @{ $real_fields }
+ );
}
=item by_key PRIMARY_KEY_VALUE
my $table = $_[0];
my(@result) = qsearch(@_);
cluck "warning: Multiple records in scalar search ($table)"
+ #.join(' / ', map "$_=>".$_[1]->{$_}, keys %{ $_[1] } )
if scalar(@result) > 1;
#should warn more vehemently if the search was on a primary key?
scalar(@result) ? ($result[0]) : ();
exists($self->{Hash}->{$field});
}
-=item AUTLOADED METHODS
+=item AUTOLOADED METHODS
$record->column is a synonym for $record->get('column');
confess "errant AUTOLOAD $field for $self (arg $value)"
unless blessed($self) && $self->can('setfield');
- #$fk_method_cache{$self->table} ||= fk_methods($self->table);
- if ( exists($fk_method_cache{$self->table}->{$field}) ) {
+ if ( my $fk_info = get_fk_method($self->table, $field) ) {
- my $fk_info = $fk_method_cache{$self->table}->{$field};
my $method = $fk_info->{method} || 'qsearchs';
my $table = $fk_info->{table} || $field;
my $column = $fk_info->{column};
# }
#}
+# get_fk_method(TABLE, FIELD)
+# Internal subroutine for fetching the foreign key descriptor for TABLE.FIELD
+# if there is one. If not, returns undef.
+# This will initialize fk_method_cache if it hasn't happened yet. It is the
+# _only_ allowed way to access the contents of %fk_method_cache.
+
+# if we wanted to be even more efficient we'd create the fk methods in the
+# symbol table instead of relying on AUTOLOAD every time
+
+sub get_fk_method {
+ my ($table, $field) = @_;
+
+ # maybe should only load one table at a time?
+ fk_methods_init() unless exists($fk_method_cache{$table});
+
+ if ( exists($fk_method_cache{$table}) and
+ exists($fk_method_cache{$table}{$field}) ) {
+ return $fk_method_cache{$table}{$field};
+ } else {
+ return undef;
+ }
+
+}
+
+sub fk_methods_init {
+ warn "[fk_methods_init]\n" if $DEBUG;
+ foreach my $table ( dbdef->tables ) {
+ $fk_method_cache{$table} = fk_methods($table);
+ }
+}
+
sub fk_methods {
my $table = shift;
# (alas. why we're cached. still, might this loop better be done once at
# schema load time insetad of every time we AUTOLOAD a method on a new
# class?)
- foreach my $f_table ( dbdef->tables ) {
- foreach my $fk (dbdef->table($f_table)->foreign_keys) {
-
- next unless $fk->table eq $table;
-
+ if (! defined $fk_table_cache) {
+ foreach my $f_table ( dbdef->tables ) {
+ foreach my $fk (dbdef->table($f_table)->foreign_keys) {
+ push @{$fk_table_cache->{$fk->table}},[$f_table,$fk];
+ }
+ }
+ }
+ foreach my $fks (@{$fk_table_cache->{$table}}) {
+ my ($f_table,$fk) = @$fks;
my $method = '';
if ( scalar( @{$fk->columns} ) == 1 ) {
if ( ! defined($fk->references)
}
}
-
- }
-
}
\%hash;
my $table = $self->table;
# Encrypt before the database
- if ( defined(eval '@FS::'. $table . '::encrypted_fields')
- && scalar( eval '@FS::'. $table . '::encrypted_fields')
- && $conf->exists('encryption')
+ if ( scalar( eval '@FS::'. $table . '::encrypted_fields')
+ && $conf_encryption
) {
foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
next if $field eq 'payinfo'
grep { defined($self->getfield($_)) && $self->getfield($_) ne "" }
real_fields($table)
;
- my @values = map { _quote( $self->getfield($_), $table, $_) } @real_fields;
- #eslaf
my $statement = "INSERT INTO $table ";
- if ( @real_fields ) {
- $statement .=
- "( ".
- join( ', ', @real_fields ).
- ") VALUES (".
- join( ', ', @values ).
- ")"
- ;
- } else {
+ my @bind_values = ();
+
+ if ( ! @real_fields ) {
+
$statement .= 'DEFAULT VALUES';
+
+ } else {
+
+ if ( $use_placeholders ) {
+
+ @bind_values = map $self->getfield($_), @real_fields;
+
+ $statement .=
+ "( ".
+ join( ', ', @real_fields ).
+ ") VALUES (".
+ join( ', ', map '?', @real_fields ). # @bind_values ).
+ ")"
+ ;
+
+ } else {
+
+ my @values = map { _quote( $self->getfield($_), $table, $_) } @real_fields;
+
+ $statement .=
+ "( ".
+ join( ', ', @real_fields ).
+ ") VALUES (".
+ join( ', ', @values ).
+ ")"
+ ;
+
+ }
+
}
+
warn "[debug]$me $statement\n" if $DEBUG > 1;
my $sth = dbh->prepare($statement) or return dbh->errstr;
local $SIG{TSTP} = 'IGNORE';
local $SIG{PIPE} = 'IGNORE';
- $sth->execute or return $sth->errstr;
+ $sth->execute(@bind_values) or return $sth->errstr;
# get inserted id from the database, if applicable & needed
if ( $db_seq && ! $self->getfield($primary_key) ) {
# Encrypt for replace
my $saved = {};
- if ( $conf->exists('encryption')
- && defined(eval '@FS::'. $new->table . '::encrypted_fields')
- && scalar( eval '@FS::'. $new->table . '::encrypted_fields')
+ if ( scalar( eval '@FS::'. $new->table . '::encrypted_fields')
+ && $conf_encryption
) {
foreach my $field (eval '@FS::'. $new->table . '::encrypted_fields') {
next if $field eq 'payinfo'
format_xml_formats => $opt->{format_xml_formats},
format_asn_formats => $opt->{format_asn_formats},
format_row_callbacks => $opt->{format_row_callbacks},
+ format_hash_callbacks => $opt->{format_hash_callbacks},
#per-import
job => $job,
file => $file,
params => { map { $_ => $param->{$_} } @pass_params },
#?
default_csv => $opt->{default_csv},
+ preinsert_callback => $opt->{preinsert_callback},
postinsert_callback => $opt->{postinsert_callback},
+ insert_args_callback => $opt->{insert_args_callback},
);
if ( $opt->{'batch_namecol'} ) {
=item format_row_callbacks
+=item format_hash_callbacks - After parsing, before object creation
+
=item fields - Alternate way to specify import, specifying import fields directly as a listref
=item preinsert_callback
my( $type, $header, $sep_char,
$fixedlength_format, $xml_format, $asn_format,
- $parser_opt, $row_callback, @fields );
+ $parser_opt, $row_callback, $hash_callback, @fields );
my $postinsert_callback = '';
$postinsert_callback = $param->{'postinsert_callback'}
my $preinsert_callback = '';
$preinsert_callback = $param->{'preinsert_callback'}
if $param->{'preinsert_callback'};
+ my $insert_args_callback = '';
+ $insert_args_callback = $param->{'insert_args_callback'}
+ if $param->{'insert_args_callback'};
if ( $param->{'format'} ) {
? $param->{'format_row_callbacks'}{ $param->{'format'} }
: '';
+ $hash_callback =
+ $param->{'format_hash_callbacks'}
+ ? $param->{'format_hash_callbacks'}{ $param->{'format'} }
+ : '';
+
@fields = @{ $formats->{ $format } };
} elsif ( $param->{'fields'} ) {
$sep_char = ',';
$fixedlength_format = '';
$row_callback = '';
+ $hash_callback = '';
@fields = @{ $param->{'fields'} };
} else {
#my $job = $param->{job};
my $line;
my $imported = 0;
+ my $unique_skip = 0; #lines skipped because they're already in the system
my( $last, $min_sec ) = ( time, 5 ); #progressbar foo
while (1) {
$hash{custnum} = $2;
}
+ %hash = &{$hash_callback}(%hash) if $hash_callback;
+
#my $table = $param->{table};
my $class = "FS::$table";
}
last if exists( $param->{skiprow} );
}
+ $unique_skip++ if $param->{unique_skip}; #line is already in the system
next if exists( $param->{skiprow} );
if ( $preinsert_callback ) {
next if exists $param->{skiprow} && $param->{skiprow};
}
- my $error = $record->insert;
+ my @insert_args = ();
+ if ( $insert_args_callback ) {
+ @insert_args = &{$insert_args_callback}($record, $param);
+ }
+
+ my $error = $record->insert(@insert_args);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
unless ( $imported || $param->{empty_ok} ) {
$dbh->rollback if $oldAutoCommit;
- return "Empty file!";
+ # freeside-cdr-conexiant-import is sensitive to the text of this message
+ return $unique_skip ? "All records in file were previously imported" : "Empty file!";
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
;
# If we're encrypting then don't store the payinfo in the history
- if ( $conf && $conf->exists('encryption') && $self->table ne 'banned_pay' ) {
+ if ( $conf_encryption && $self->table ne 'banned_pay' ) {
@fields = grep { $_ ne 'payinfo' } @fields;
}
=item ut_text COLUMN
Check/untaint text. Alphanumerics, spaces, and the following punctuation
-symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < >
+symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < > ~
May not be null. If there is an error, returns the error, otherwise returns
false.
# \p{Word} = alphanumerics, marks (diacritics), and connectors
# see perldoc perluniprops
$self->getfield($field)
- =~ /^([\p{Word} \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>$money_char]+)$/
+ =~ /^([\p{Word} \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>\~$money_char]+)$/
or return gettext('illegal_or_empty_text'). " $field: ".
$self->getfield($field);
$self->setfield($field,$1);
my $coord = $self->getfield($field);
my $neg = $coord =~ s/^(-)//;
+ # ignore degree symbol at the end,
+ # but not otherwise supporting degree/minutes/seconds symbols
+ $coord =~ s/\N{DEGREE SIGN}\s*$//;
+
my ($d, $m, $s) = (0, 0, 0);
if (
sub ut_name {
my( $self, $field ) = @_;
-# warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n";
$self->getfield($field) =~ /^([\p{Word} \,\.\-\']+)$/
or return gettext('illegal_name'). " $field: ". $self->getfield($field);
my $name = $1;
$self->getfield($field);
$self->setfield($field, "$1 $2");
+ } elsif ( $country eq 'AU' ) {
+
+ $self->getfield($field) =~ /^\s*(\d{4})\s*$/
+ or return gettext('illegal_zip'). " $field for country $country: ".
+ $self->getfield($field);
+ $self->setfield($field, $1);
+
} else {
if ( $self->getfield($field) =~ /^\s*$/
sub ut_foreign_key {
my( $self, $field, $table, $foreign ) = @_;
- return '' if $no_check_foreign;
+ return $self->ut_number($field) if $no_check_foreign;
qsearchs($table, { $foreign => $self->getfield($field) })
or return "Can't find ". $self->table. ".$field ". $self->getfield($field).
" in $table.$foreign";
}
+=item trim_whitespace FIELD[, FIELD ... ]
+
+Strip leading and trailing spaces from the value in the named FIELD(s).
+
+=cut
+
+sub trim_whitespace {
+ my $self = shift;
+ foreach my $field (@_) {
+ my $value = $self->get($field);
+ $value =~ s/^\s+//;
+ $value =~ s/\s+$//;
+ $self->set($field, $value);
+ }
+}
+
=item fields [ TABLE ]
This is a wrapper for real_fields. Code that called
my ($self, $value) = @_;
my $encrypted = $value;
- if ($conf->exists('encryption')) {
+ if ($conf_encryption) {
if ($self->is_encrypted($value)) {
# Return the original value if it isn't plaintext.
$encrypted = $value;
sub decrypt {
my ($self,$value) = @_;
my $decrypted = $value; # Will return the original value if it isn't encrypted or can't be decrypted.
- if ($conf->exists('encryption') && $self->is_encrypted($value)) {
+ if ($conf_encryption && $self->is_encrypted($value)) {
$self->loadRSA;
if (ref($rsa_decrypt) =~ /::RSA/) {
my $encrypted = unpack ("u*", $value);
#Initialize the Module
$rsa_module = 'Crypt::OpenSSL::RSA'; # The Default
- if ($conf->exists('encryptionmodule') && $conf->config('encryptionmodule') ne '') {
- $rsa_module = $conf->config('encryptionmodule');
+ if ($conf_encryptionmodule && $conf_encryptionmodule ne '') {
+ $rsa_module = $conf_encryptionmodule;
}
if (!$rsa_loaded) {
$rsa_loaded++;
}
# Initialize Encryption
- if ($conf->exists('encryptionpublickey') && $conf->config('encryptionpublickey') ne '') {
- my $public_key = join("\n",$conf->config('encryptionpublickey'));
- $rsa_encrypt = $rsa_module->new_public_key($public_key);
+ if ($conf_encryptionpublickey && $conf_encryptionpublickey ne '') {
+ $rsa_encrypt = $rsa_module->new_public_key($conf_encryptionpublickey);
}
# Intitalize Decryption
- if ($conf->exists('encryptionprivatekey') && $conf->config('encryptionprivatekey') ne '') {
- my $private_key = join("\n",$conf->config('encryptionprivatekey'));
- $rsa_decrypt = $rsa_module->new_private_key($private_key);
+ if ($conf_encryptionprivatekey && $conf_encryptionprivatekey ne '') {
+ $rsa_decrypt = $rsa_module->new_private_key($conf_encryptionprivatekey);
}
}
$self->scalar_sql($sql, @_);
}
+=item row_exists [ WHERE [, PLACEHOLDER ...] ]
+
+Convenience method for the common case of "SELECT 1 FROM table ... LIMIT 1"
+with optional (but almost always needed) WHERE.
+
+=cut
+
+sub row_exists {
+ my($self, $where) = (shift, shift);
+ my $table = $self->table or die 'row_exists called on object of class '.ref($self);
+ my $sql = "SELECT 1 FROM $table";
+ $sql .= " WHERE $where" if $where;
+ $sql .= " LIMIT 1";
+ $self->scalar_sql($sql, @_);
+}
+
=back
=head1 SUBROUTINES
&& driver_name eq 'Pg'
)
{
- no strict 'subs';
-# dbh->quote($value, { pg_type => PG_BYTEA() }); # doesn't work right
- # Pg binary string quoting: convert each character to 3-digit octal prefixed with \\,
- # single-quote the whole mess, and put an "E" in front.
- return ("E'" . join('', map { sprintf('\\\\%03o', ord($_)) } split(//, $value) ) . "'");
+ dbh->quote($value, { pg_type => PG_BYTEA() });
} else {
dbh->quote($value);
}