use strict;
use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG
- $conf $conf_encryption $me
%virtual_fields_cache
+ $conf $conf_encryption $money_char $lat_lower $lon_upper
+ $me
$nowarn_identical $nowarn_classload
- $no_update_diff $no_check_foreign
+ $no_update_diff $no_history $no_check_foreign
+ @encrypt_payby
);
use Exporter;
use Carp qw(carp cluck croak confess);
use FS::Schema qw(dbdef);
use FS::SearchCache;
use FS::Msgcat qw(gettext);
+use NetAddr::IP; # for validation
#use FS::Conf; #dependency loop bs, in install_callback below instead
use FS::part_virtual_field;
@ISA = qw(Exporter);
+@encrypt_payby = qw( CARD DCRD CHEK DCHK );
+
#export dbdef for now... everything else expects to find it here
@EXPORT_OK = qw(
dbh fields hfields qsearch qsearchs dbdef jsearch
str2time_sql str2time_sql_closing regexp_sql not_regexp_sql concat_sql
+ midnight_sql
);
$DEBUG = 0;
$nowarn_identical = 0;
$nowarn_classload = 0;
$no_update_diff = 0;
+$no_history = 0;
$no_check_foreign = 0;
my $rsa_module;
$conf = '';
$conf_encryption = '';
FS::UID->install_callback( sub {
+
eval "use FS::Conf;";
die $@ if $@;
$conf = FS::Conf->new;
$conf_encryption = $conf->exists('encryption');
+ $money_char = $conf->config('money_char') || '$';
+ my $nw_coords = $conf->exists('geocode-require_nw_coordinates');
+ $lat_lower = $nw_coords ? 1 : -90;
+ $lon_upper = $nw_coords ? -1 : 180;
+
$File::CounterFile::DEFAULT_DIR = $conf->base_dir . "/counters.". datasrc;
+
if ( driver_name eq 'Pg' ) {
eval "use DBD::Pg ':pg_types'";
die $@ if $@;
} else {
eval "sub PG_BYTEA { die 'guru meditation #9: calling PG_BYTEA when not running Pg?'; }";
}
+
} );
=head1 NAME
my $bind_type = { TYPE => SQL_VARCHAR };
- if ( $type =~ /(big)?(int|serial)/i && $value =~ /^\d+(\.\d+)?$/ ) {
+ if ( $type =~ /(big)?(int|serial)/i && $value =~ /^-?\d+(\.\d+)?$/ ) {
$bind_type = { TYPE => SQL_INTEGER };
&& eval 'defined(@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)));
}
qq-( $column $op "" )-;
}
}
+ } elsif ( $op eq '!=' ) {
+ qq-( $column IS NULL OR $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
my $error = $self->check;
return $error if $error;
- #single-field unique keys are given a value if false
+ #single-field non-null unique keys are given a value if empty
#(like MySQL's AUTO_INCREMENT or Pg SERIAL)
foreach ( $self->dbdef_table->unique_singles) {
- $self->unique($_) unless $self->getfield($_);
+ next if $self->getfield($_);
+ next if $self->dbdef_table->column($_)->null eq 'NULL';
+ $self->unique($_);
}
#and also the primary key, if the database isn't going to
&& $conf->exists('encryption')
) {
foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
- $self->{'saved'} = $self->getfield($field);
+ next if $field eq 'payinfo'
+ && ($self->isa('FS::payinfo_transaction_Mixin')
+ || $self->isa('FS::payinfo_Mixin') )
+ && $self->payby
+ && !grep { $self->payby eq $_ } @encrypt_payby;
+ $saved->{$field} = $self->getfield($field);
$self->setfield($field, $self->encrypt($self->getfield($field)));
}
}
&& scalar( eval '@FS::'. $new->table . '::encrypted_fields')
) {
foreach my $field (eval '@FS::'. $new->table . '::encrypted_fields') {
+ next if $field eq 'payinfo'
+ && ($new->isa('FS::payinfo_transaction_Mixin')
+ || $new->isa('FS::payinfo_Mixin') )
+ && $new->payby
+ && !grep { $new->payby eq $_ } @encrypt_payby;
$saved->{$field} = $new->getfield($field);
$new->setfield($field, $new->encrypt($new->getfield($field)));
}
? ($_, $new->getfield($_)) : () } $old->fields;
unless (keys(%diff) || $no_update_diff ) {
- carp "[warning]$me $new -> replace $old: records identical"
+ carp "[warning]$me ". ref($new)."->replace ".
+ ( $primary_key ? "$primary_key ".$new->get($primary_key) : '' ).
+ ": records identical"
unless $nowarn_identical;
return '';
}
params => { map { $_ => $param->{$_} } @pass_params },
#?
default_csv => $opt->{default_csv},
+ postinsert_callback => $opt->{postinsert_callback},
);
if ( $opt->{'batch_namecol'} ) {
=item fields - Alternate way to specify import, specifying import fields directly as a listref
+=item preinsert_callback
+
=item postinsert_callback
=item params
my( $type, $header, $sep_char, $fixedlength_format,
$xml_format, $row_callback, @fields );
+
my $postinsert_callback = '';
+ $postinsert_callback = $param->{'postinsert_callback'}
+ if $param->{'postinsert_callback'};
+ my $preinsert_callback = '';
+ $preinsert_callback = $param->{'preinsert_callback'}
+ if $param->{'preinsert_callback'};
+
if ( $param->{'format'} ) {
my $format = $param->{'format'};
$row_callback = '';
@fields = @{ $param->{'fields'} };
- $postinsert_callback = $param->{'postinsert_callback'}
- if $param->{'postinsert_callback'}
-
} else {
die "neither format nor fields specified";
}
if ( $type eq 'csv' ) {
- my %attr = ();
+ my %attr = ( 'binary' => 1, );
$attr{sep_char} = $sep_char if $sep_char;
$parser = new Text::CSV_XS \%attr;
eval "use Parse::FixedLength;";
die $@ if $@;
- $parser = new Parse::FixedLength $fixedlength_format;
+ $parser = Parse::FixedLength->new($fixedlength_format);
}
else {
next if $line =~ /^\s*$/; #skip empty lines
$line = &{$row_callback}($line) if $row_callback;
+
+ next if $line =~ /^\s*$/; #skip empty lines
$parser->parse($line) or do {
$dbh->rollback if $oldAutoCommit;
- return "can't parse: ". $parser->error_input();
+ return "can't parse: ". $parser->error_input() . " " . $parser->error_diag;
};
@columns = $parser->fields();
} elsif ( $type eq 'fixedlength' ) {
+ last unless scalar(@buffer);
+ $line = shift(@buffer);
+
@columns = $parser->parse($line);
} elsif ( $type eq 'xls' ) {
}
next if exists( $param->{skiprow} );
+ if ( $preinsert_callback ) {
+ my $error = &{$preinsert_callback}($record, $param);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "preinsert_callback error". ( $line ? " for $line" : '' ).
+ ": $error";
+ }
+ next if exists $param->{skiprow} && $param->{skiprow};
+ }
+
my $error = $record->insert;
if ( $error ) {
;
# If we're encrypting then don't store the payinfo in the history
- if ( $conf && $conf->exists('encryption') ) {
+ if ( $conf && $conf->exists('encryption') && $self->table ne 'banned_pay' ) {
@fields = grep { $_ ne 'payinfo' } @fields;
}
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);
+ }
+
'';
}
#warn "notexist ". \¬exist. "\n";
#warn "AUTOLOAD ". \&AUTOLOAD. "\n";
$self->getfield($field)
- =~ /^([µ_0123456789aAáÁàÀâÂåÅäÄãêæÆbBcCçÇdDðÐeEéÉèÈêÊëËfFgGhHiIíÍìÌîÎïÏjJkKlLmMnNñÑoOóÓòÒôÔöÖõÕøغpPqQrRsSßtTuUúÚùÙûÛüÜvVwWxXyYýÝÿzZþÞ \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>]+)$/
+ =~ /^([\wô \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>$money_char]+)$/
or return gettext('illegal_or_empty_text'). " $field: ".
$self->getfield($field);
$self->setfield($field,$1);
=item ut_textn COLUMN
Check/untaint text. Alphanumerics, spaces, and the following punctuation
-symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? /
+symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < >
May be null. If there is an error, returns the error, otherwise returns false.
=cut
$self->setfield($field, uc($1));
'';
}
+
+=item ut_mac_addr COLUMN
+
+Check/untaint mac addresses. May be null.
+
+=cut
+
+sub ut_mac_addr {
+ my($self, $field) = @_;
+
+ my $mac = $self->get($field);
+ $mac =~ s/\s+//g;
+ $mac =~ s/://g;
+ $self->set($field, $mac);
+
+ my $e = $self->ut_hex($field);
+ return $e if $e;
+
+ return "Illegal (mac address) $field: ". $self->getfield($field)
+ unless length($self->getfield($field)) == 12;
+
+ '';
+
+}
+
+=item ut_mac_addrn COLUMN
+
+Check/untaint mac addresses. May be null.
+
+=cut
+
+sub ut_mac_addrn {
+ my($self, $field) = @_;
+ ($self->getfield($field) eq '') ? '' : $self->ut_mac_addr($field);
+}
+
=item ut_ip COLUMN
Check/untaint ip addresses. IPv4 only for now, though ::1 is auto-translated
}
}
+=item ut_ip46 COLUMN
+
+Check/untaint IPv4 or IPv6 address.
+
+=cut
+
+sub ut_ip46 {
+ my( $self, $field ) = @_;
+ my $ip = NetAddr::IP->new($self->getfield($field))
+ or return "Illegal (IP address) $field: ".$self->getfield($field);
+ $self->setfield($field, lc($ip->addr));
+ return '';
+}
+
+=item ut_ip46n
+
+Check/untaint IPv6 or IPv6 address. May be null.
+
+=cut
+
+sub ut_ip46n {
+ my( $self, $field ) = @_;
+ if ( $self->getfield($field) =~ /^$/ ) {
+ $self->setfield($field, '');
+ return '';
+ }
+ $self->ut_ip46($field);
+}
+
=item ut_coord COLUMN [ LOWER [ UPPER ] ]
Check/untaint coordinates.
=cut
sub ut_coord {
-
my ($self, $field) = (shift, shift);
- my $lower = shift if scalar(@_);
- my $upper = shift if scalar(@_);
+ my($lower, $upper);
+ if ( $field =~ /latitude/ ) {
+ $lower = $lat_lower;
+ $upper = 90;
+ } elsif ( $field =~ /longitude/ ) {
+ $lower = -180;
+ $upper = $lon_upper;
+ }
+
my $coord = $self->getfield($field);
my $neg = $coord =~ s/^(-)//;
my ($self, $field) = (shift, shift);
- if ($self->getfield($field) =~ /^$/) {
+ if ($self->getfield($field) =~ /^\s*$/) {
return '';
} else {
return $self->ut_coord($field, @_);
sub ut_name {
my( $self, $field ) = @_;
# warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n";
- #$self->getfield($field) =~ /^([\w \,\.\-\']+)$/
- $self->getfield($field) =~ /^([µ_0123456789aAáÁàÀâÂåÅäÄãêæÆbBcCçÇdDðÐeEéÉèÈêÊëËfFgGhHiIíÍìÌîÎïÏjJkKlLmMnNñÑoOóÓòÒôÔöÖõÕøغpPqQrRsSßtTuUúÚùÙûÛüÜvVwWxXyYýÝÿzZþÞ \,\.\-\']+)$/
+ $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.
{
$self->setfield($field,'');
} else {
- $self->getfield($field) =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
+ $self->getfield($field) =~ /^\s*(\w[\w\-\s]{0,8}\w)\s*$/
or return gettext('illegal_zip'). " $field: ". $self->getfield($field);
$self->setfield($field,$1);
}
defined($scalar) ? $scalar : '';
}
+=item count [ WHERE ]
+
+Convenience method for the common case of "SELECT COUNT(*) FROM table",
+with optional WHERE. Must be called as method on a class with an
+associated table.
+
+=cut
+
+sub count {
+ my($self, $where) = (shift, shift);
+ my $table = $self->table or die 'count called on object of class '.ref($self);
+ my $sql = "SELECT COUNT(*) FROM $table";
+ $sql .= " WHERE $where" if $where;
+ $self->scalar_sql($sql);
+}
+
=back
=head1 SUBROUTINES
=item concat_sql [ DRIVER_NAME ] ITEMS_ARRAYREF
-Returns the items concatendated based on database type, using "CONCAT()" for
+Returns the items concatenated based on database type, using "CONCAT()" for
mysql and " || " for Pg and other databases.
You can pass an optional driver name such as "Pg", "mysql" or
}
+=item midnight_sql DATE
+
+Returns an SQL expression to convert DATE (a unix timestamp) to midnight
+on that day in the system timezone, using the default driver name.
+
+=cut
+
+sub midnight_sql {
+ my $driver = driver_name;
+ my $expr = shift;
+ if ( $driver =~ /^mysql/i ) {
+ "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME($expr)))";
+ }
+ else {
+ "EXTRACT( EPOCH FROM DATE(TO_TIMESTAMP($expr)) )";
+ }
+}
+
=back
=head1 BUGS