support listref to qsearch as UNION
[freeside.git] / FS / FS / Record.pm
1 package FS::Record;
2
3 use strict;
4 use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG
5              $conf $conf_encryption $me
6              %virtual_fields_cache
7              $nowarn_identical $nowarn_classload
8              $no_update_diff $no_check_foreign
9            );
10 use Exporter;
11 use Carp qw(carp cluck croak confess);
12 use Scalar::Util qw( blessed );
13 use File::CounterFile;
14 use Locale::Country;
15 use Text::CSV_XS;
16 use File::Slurp qw( slurp );
17 use DBI qw(:sql_types);
18 use DBIx::DBSchema 0.33;
19 use FS::UID qw(dbh getotaker datasrc driver_name);
20 use FS::CurrentUser;
21 use FS::Schema qw(dbdef);
22 use FS::SearchCache;
23 use FS::Msgcat qw(gettext);
24 #use FS::Conf; #dependency loop bs, in install_callback below instead
25
26 use FS::part_virtual_field;
27
28 use Tie::IxHash;
29
30 @ISA = qw(Exporter);
31
32 #export dbdef for now... everything else expects to find it here
33 @EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef jsearch
34                 str2time_sql str2time_sql_closing );
35
36 $DEBUG = 0;
37 $me = '[FS::Record]';
38
39 $nowarn_identical = 0;
40 $nowarn_classload = 0;
41 $no_update_diff = 0;
42 $no_check_foreign = 0;
43
44 my $rsa_module;
45 my $rsa_loaded;
46 my $rsa_encrypt;
47 my $rsa_decrypt;
48
49 $conf = '';
50 $conf_encryption = '';
51 FS::UID->install_callback( sub {
52   eval "use FS::Conf;";
53   die $@ if $@;
54   $conf = FS::Conf->new; 
55   $conf_encryption = $conf->exists('encryption');
56   $File::CounterFile::DEFAULT_DIR = $conf->base_dir . "/counters.". datasrc;
57   if ( driver_name eq 'Pg' ) {
58     eval "use DBD::Pg ':pg_types'";
59     die $@ if $@;
60   } else {
61     eval "sub PG_BYTEA { die 'guru meditation #9: calling PG_BYTEA when not running Pg?'; }";
62   }
63 } );
64
65 =head1 NAME
66
67 FS::Record - Database record objects
68
69 =head1 SYNOPSIS
70
71     use FS::Record;
72     use FS::Record qw(dbh fields qsearch qsearchs);
73
74     $record = new FS::Record 'table', \%hash;
75     $record = new FS::Record 'table', { 'column' => 'value', ... };
76
77     $record  = qsearchs FS::Record 'table', \%hash;
78     $record  = qsearchs FS::Record 'table', { 'column' => 'value', ... };
79     @records = qsearch  FS::Record 'table', \%hash; 
80     @records = qsearch  FS::Record 'table', { 'column' => 'value', ... };
81
82     $table = $record->table;
83     $dbdef_table = $record->dbdef_table;
84
85     $value = $record->get('column');
86     $value = $record->getfield('column');
87     $value = $record->column;
88
89     $record->set( 'column' => 'value' );
90     $record->setfield( 'column' => 'value' );
91     $record->column('value');
92
93     %hash = $record->hash;
94
95     $hashref = $record->hashref;
96
97     $error = $record->insert;
98
99     $error = $record->delete;
100
101     $error = $new_record->replace($old_record);
102
103     # external use deprecated - handled by the database (at least for Pg, mysql)
104     $value = $record->unique('column');
105
106     $error = $record->ut_float('column');
107     $error = $record->ut_floatn('column');
108     $error = $record->ut_number('column');
109     $error = $record->ut_numbern('column');
110     $error = $record->ut_snumber('column');
111     $error = $record->ut_snumbern('column');
112     $error = $record->ut_money('column');
113     $error = $record->ut_text('column');
114     $error = $record->ut_textn('column');
115     $error = $record->ut_alpha('column');
116     $error = $record->ut_alphan('column');
117     $error = $record->ut_phonen('column');
118     $error = $record->ut_anything('column');
119     $error = $record->ut_name('column');
120
121     $quoted_value = _quote($value,'table','field');
122
123     #deprecated
124     $fields = hfields('table');
125     if ( $fields->{Field} ) { # etc.
126
127     @fields = fields 'table'; #as a subroutine
128     @fields = $record->fields; #as a method call
129
130
131 =head1 DESCRIPTION
132
133 (Mostly) object-oriented interface to database records.  Records are currently
134 implemented on top of DBI.  FS::Record is intended as a base class for
135 table-specific classes to inherit from, i.e. FS::cust_main.
136
137 =head1 CONSTRUCTORS
138
139 =over 4
140
141 =item new [ TABLE, ] HASHREF
142
143 Creates a new record.  It doesn't store it in the database, though.  See
144 L<"insert"> for that.
145
146 Note that the object stores this hash reference, not a distinct copy of the
147 hash it points to.  You can ask the object for a copy with the I<hash> 
148 method.
149
150 TABLE can only be omitted when a dervived class overrides the table method.
151
152 =cut
153
154 sub new { 
155   my $proto = shift;
156   my $class = ref($proto) || $proto;
157   my $self = {};
158   bless ($self, $class);
159
160   unless ( defined ( $self->table ) ) {
161     $self->{'Table'} = shift;
162     carp "warning: FS::Record::new called with table name ". $self->{'Table'}
163       unless $nowarn_classload;
164   }
165   
166   $self->{'Hash'} = shift;
167
168   foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) { 
169     $self->{'Hash'}{$field}='';
170   }
171
172   $self->_rebless if $self->can('_rebless');
173
174   $self->{'modified'} = 0;
175
176   $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
177
178   $self;
179 }
180
181 sub new_or_cached {
182   my $proto = shift;
183   my $class = ref($proto) || $proto;
184   my $self = {};
185   bless ($self, $class);
186
187   $self->{'Table'} = shift unless defined ( $self->table );
188
189   my $hashref = $self->{'Hash'} = shift;
190   my $cache = shift;
191   if ( defined( $cache->cache->{$hashref->{$cache->key}} ) ) {
192     my $obj = $cache->cache->{$hashref->{$cache->key}};
193     $obj->_cache($hashref, $cache) if $obj->can('_cache');
194     $obj;
195   } else {
196     $cache->cache->{$hashref->{$cache->key}} = $self->new($hashref, $cache);
197   }
198
199 }
200
201 sub create {
202   my $proto = shift;
203   my $class = ref($proto) || $proto;
204   my $self = {};
205   bless ($self, $class);
206   if ( defined $self->table ) {
207     cluck "create constructor is deprecated, use new!";
208     $self->new(@_);
209   } else {
210     croak "FS::Record::create called (not from a subclass)!";
211   }
212 }
213
214 =item qsearch PARAMS_HASHREF | TABLE, HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ, ADDL_FROM
215
216 Searches the database for all records matching (at least) the key/value pairs
217 in HASHREF.  Returns all the records found as `FS::TABLE' objects if that
218 module is loaded (i.e. via `use FS::cust_main;'), otherwise returns FS::Record
219 objects.
220
221 The preferred usage is to pass a hash reference of named parameters:
222
223   @records = qsearch( {
224                         'table'       => 'table_name',
225                         'hashref'     => { 'field' => 'value'
226                                            'field' => { 'op'    => '<',
227                                                         'value' => '420',
228                                                       },
229                                          },
230
231                         #these are optional...
232                         'select'      => '*',
233                         'extra_sql'   => 'AND field = ? AND intfield = ?',
234                         'extra_param' => [ 'value', [ 5, 'int' ] ],
235                         'order_by'    => 'ORDER BY something',
236                         #'cache_obj'   => '', #optional
237                         'addl_from'   => 'LEFT JOIN othtable USING ( field )',
238                         'debug'       => 1,
239                       }
240                     );
241
242 Much code still uses old-style positional parameters, this is also probably
243 fine in the common case where there are only two parameters:
244
245   my @records = qsearch( 'table', { 'field' => 'value' } );
246
247 Also possible is an experimental LISTREF of PARAMS_HASHREFs for a UNION of
248 the individual PARAMS_HASHREF queries
249
250 ###oops, argh, FS::Record::new only lets us create database fields.
251 #Normal behaviour if SELECT is not specified is `*', as in
252 #C<SELECT * FROM table WHERE ...>.  However, there is an experimental new
253 #feature where you can specify SELECT - remember, the objects returned,
254 #although blessed into the appropriate `FS::TABLE' package, will only have the
255 #fields you specify.  This might have unwanted results if you then go calling
256 #regular FS::TABLE methods
257 #on it.
258
259 =cut
260
261 my %TYPE = (); #for debugging
262
263 sub _bind_type {
264   my($type, $value) = @_;
265
266   my $bind_type = { TYPE => SQL_VARCHAR };
267
268   if ( $type =~ /(big)?(int|serial)/i && $value =~ /^\d+(\.\d+)?$/ ) {
269
270     $bind_type = { TYPE => SQL_INTEGER };
271
272   } elsif ( $type =~ /^bytea$/i || $type =~ /(blob|varbinary)/i ) {
273
274     if ( driver_name eq 'Pg' ) {
275       no strict 'subs';
276       $bind_type = { pg_type => PG_BYTEA };
277     #} else {
278     #  $bind_type = ? #SQL_VARCHAR could be fine?
279     }
280
281   #DBD::Pg 1.49: Cannot bind ... unknown sql_type 6 with SQL_FLOAT
282   #fixed by DBD::Pg 2.11.8
283   #can change back to SQL_FLOAT in early-mid 2010, once everyone's upgraded
284   #(make a Tron test first)
285   } elsif ( _is_fs_float( $type, $value ) ) {
286
287     $bind_type = { TYPE => SQL_DECIMAL };
288
289   }
290
291   $bind_type;
292
293 }
294
295 sub _is_fs_float {
296   my($type, $value) = @_;
297   if ( ( $type =~ /(numeric)/i && $value =~ /^[+-]?\d+(\.\d+)?$/ ) ||
298        ( $type =~ /(real|float4)/i && $value =~ /[-+]?\d*\.?\d+([eE][-+]?\d+)?/)
299      ) {
300     return 1;
301   }
302   '';
303 }
304
305 sub qsearch {
306   my( @stable, @record, @cache );
307   my( @select, @extra_sql, @extra_param, @order_by, @addl_from );
308   my @debug = ();
309   my %union_options = ();
310   if ( ref($_[0]) eq 'ARRAY' ) {
311     my $optlist = shift;
312     %union_options = @_;
313     foreach my $href ( @$optlist ) {
314       push @stable,      ( $href->{'table'} or die "table name is required" );
315       push @record,      ( $href->{'hashref'}     || {} );
316       push @select,      ( $href->{'select'}      || '*' );
317       push @extra_sql,   ( $href->{'extra_sql'}   || '' );
318       push @extra_param, ( $href->{'extra_param'} || [] );
319       push @order_by,    ( $href->{'order_by'}    || '' );
320       push @cache,       ( $href->{'cache_obj'}   || '' );
321       push @addl_from,   ( $href->{'addl_from'}   || '' );
322       push @debug,       ( $href->{'debug'}       || '' );
323     }
324     die "at least one hashref is required" unless scalar(@stable);
325   } elsif ( ref($_[0]) eq 'HASH' ) {
326     my $opt = shift;
327     $stable[0]      = $opt->{'table'}       or die "table name is required";
328     $record[0]      = $opt->{'hashref'}     || {};
329     $select[0]      = $opt->{'select'}      || '*';
330     $extra_sql[0]   = $opt->{'extra_sql'}   || '';
331     $extra_param[0] = $opt->{'extra_param'} || [];
332     $order_by[0]    = $opt->{'order_by'}    || '';
333     $cache[0]       = $opt->{'cache_obj'}   || '';
334     $addl_from[0]   = $opt->{'addl_from'}   || '';
335     $debug[0]       = $opt->{'debug'}       || '';
336   } else {
337     ( $stable[0],
338       $record[0],
339       $select[0],
340       $extra_sql[0],
341       $cache[0],
342       $addl_from[0]
343     ) = @_;
344     $select[0] ||= '*';
345   }
346   my $cache = $cache[0];
347
348   my @statement = ();
349   my @value = ();
350   my @bind_type = ();
351   my $dbh = dbh;
352   foreach my $stable ( @stable ) {
353     my $record      = shift @record;
354     my $select      = shift @select;
355     my $extra_sql   = shift @extra_sql;
356     my $extra_param = shift @extra_param;
357     my $order_by    = shift @order_by;
358     my $cache       = shift @cache;
359     my $addl_from   = shift @addl_from;
360     my $debug       = shift @debug;
361
362     #$stable =~ /^([\w\_]+)$/ or die "Illegal table: $table";
363     #for jsearch
364     $stable =~ /^([\w\s\(\)\.\,\=]+)$/ or die "Illegal table: $stable";
365     $stable = $1;
366
367     my $table = $cache ? $cache->table : $stable;
368     my $dbdef_table = dbdef->table($table)
369       or die "No schema for table $table found - ".
370              "do you need to run freeside-upgrade?";
371     my $pkey = $dbdef_table->primary_key;
372
373     my @real_fields = grep exists($record->{$_}), real_fields($table);
374     my @virtual_fields;
375     if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
376       @virtual_fields = grep exists($record->{$_}), "FS::$table"->virtual_fields;
377     } else {
378       cluck "warning: FS::$table not loaded; virtual fields not searchable"
379         unless $nowarn_classload;
380       @virtual_fields = ();
381     }
382
383     my $statement .= "SELECT $select FROM $stable";
384     $statement .= " $addl_from" if $addl_from;
385     if ( @real_fields or @virtual_fields ) {
386       $statement .= ' WHERE '. join(' AND ',
387         get_real_fields($table, $record, \@real_fields) ,
388         get_virtual_fields($table, $pkey, $record, \@virtual_fields),
389         );
390     }
391
392     $statement .= " $extra_sql" if defined($extra_sql);
393     $statement .= " $order_by"  if defined($order_by);
394
395     push @statement, $statement;
396
397     warn "[debug]$me $statement\n" if $DEBUG > 1 || $debug;
398  
399
400     foreach my $field (
401       grep defined( $record->{$_} ) && $record->{$_} ne '', @real_fields
402     ) {
403
404       my $value = $record->{$field};
405       my $op = (ref($value) && $value->{op}) ? $value->{op} : '=';
406       $value = $value->{'value'} if ref($value);
407       my $type = dbdef->table($table)->column($field)->type;
408
409       my $bind_type = _bind_type($type, $value);
410
411       #if ( $DEBUG > 2 ) {
412       #  no strict 'refs';
413       #  %TYPE = map { &{"DBI::$_"}() => $_ } @{ $DBI::EXPORT_TAGS{sql_types} }
414       #    unless keys %TYPE;
415       #  warn "  bind_param $bind (for field $field), $value, TYPE $TYPE{$TYPE}\n";
416       #}
417
418       push @value, $value;
419       push @bind_type, $bind_type;
420
421     }
422
423     foreach my $param ( @$extra_param ) {
424       my $bind_type = { TYPE => SQL_VARCHAR };
425       my $value = $param;
426       if ( ref($param) ) {
427         $value = $param->[0];
428         my $type = $param->[1];
429         $bind_type = _bind_type($type, $value);
430       }
431       push @value, $value;
432       push @bind_type, $bind_type;
433     }
434   }
435
436   my $statement = join( ' ) UNION ( ', @statement );
437   $statement = "( $statement )" if scalar(@statement) > 1;
438   $statement .= " $union_options{order_by}" if $union_options{order_by};
439
440   my $sth = $dbh->prepare($statement)
441     or croak "$dbh->errstr doing $statement";
442
443   my $bind = 1;
444   foreach my $value ( @value ) {
445     my $bind_type = shift @bind_type;
446     $sth->bind_param($bind++, $value, $bind_type );
447   }
448
449 #  $sth->execute( map $record->{$_},
450 #    grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
451 #  ) or croak "Error executing \"$statement\": ". $sth->errstr;
452
453   $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
454
455   # virtual fields and blessings are nonsense in a heterogeneous UNION, right?
456   my $table = $stable[0];
457   my $pkey = '';
458   $table = '' if grep { $_ ne $table } @stable;
459   $pkey = dbdef->table($table)->primary_key if $table;
460
461   my @virtual_fields = ();
462   if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
463     @virtual_fields = "FS::$table"->virtual_fields;
464   } else {
465     cluck "warning: FS::$table not loaded; virtual fields not returned either"
466       unless $nowarn_classload;
467     @virtual_fields = ();
468   }
469
470   my %result;
471   tie %result, "Tie::IxHash";
472   my @stuff = @{ $sth->fetchall_arrayref( {} ) };
473   if ( $pkey && scalar(@stuff) && $stuff[0]->{$pkey} ) {
474     %result = map { $_->{$pkey}, $_ } @stuff;
475   } else {
476     @result{@stuff} = @stuff;
477   }
478
479   $sth->finish;
480
481   if ( keys(%result) and @virtual_fields ) {
482     $statement =
483       "SELECT virtual_field.recnum, part_virtual_field.name, ".
484              "virtual_field.value ".
485       "FROM part_virtual_field JOIN virtual_field USING (vfieldpart) ".
486       "WHERE part_virtual_field.dbtable = '$table' AND ".
487       "virtual_field.recnum IN (".
488       join(',', keys(%result)). ") AND part_virtual_field.name IN ('".
489       join(q!', '!, @virtual_fields) . "')";
490     warn "[debug]$me $statement\n" if $DEBUG > 1;
491     $sth = $dbh->prepare($statement) or croak "$dbh->errstr doing $statement";
492     $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
493
494     foreach (@{ $sth->fetchall_arrayref({}) }) {
495       my $recnum = $_->{recnum};
496       my $name = $_->{name};
497       my $value = $_->{value};
498       if (exists($result{$recnum})) {
499         $result{$recnum}->{$name} = $value;
500       }
501     }
502   }
503   my @return;
504   if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
505     if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
506       #derivied class didn't override new method, so this optimization is safe
507       if ( $cache ) {
508         @return = map {
509           new_or_cached( "FS::$table", { %{$_} }, $cache )
510         } values(%result);
511       } else {
512         @return = map {
513           new( "FS::$table", { %{$_} } )
514         } values(%result);
515       }
516     } else {
517       #okay, its been tested
518       # warn "untested code (class FS::$table uses custom new method)";
519       @return = map {
520         eval 'FS::'. $table. '->new( { %{$_} } )';
521       } values(%result);
522     }
523
524     # Check for encrypted fields and decrypt them.
525    ## only in the local copy, not the cached object
526     if ( $conf_encryption 
527          && eval 'defined(@FS::'. $table . '::encrypted_fields)' ) {
528       foreach my $record (@return) {
529         foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
530           # Set it directly... This may cause a problem in the future...
531           $record->setfield($field, $record->decrypt($record->getfield($field)));
532         }
533       }
534     }
535   } else {
536     cluck "warning: FS::$table not loaded; returning FS::Record objects"
537       unless $nowarn_classload;
538     @return = map {
539       FS::Record->new( $table, { %{$_} } );
540     } values(%result);
541   }
542   return @return;
543 }
544
545 ## makes this easier to read
546
547 sub get_virtual_fields {
548    my $table = shift;
549    my $pkey = shift;
550    my $record = shift;
551    my $virtual_fields = shift;
552    
553    return
554     ( map {
555       my $op = '=';
556       my $column = $_;
557       if ( ref($record->{$_}) ) {
558         $op = $record->{$_}{'op'} if $record->{$_}{'op'};
559         if ( uc($op) eq 'ILIKE' ) {
560           $op = 'LIKE';
561           $record->{$_}{'value'} = lc($record->{$_}{'value'});
562           $column = "LOWER($_)";
563         }
564         $record->{$_} = $record->{$_}{'value'};
565       }
566
567       # ... EXISTS ( SELECT name, value FROM part_virtual_field
568       #              JOIN virtual_field
569       #              ON part_virtual_field.vfieldpart = virtual_field.vfieldpart
570       #              WHERE recnum = svc_acct.svcnum
571       #              AND (name, value) = ('egad', 'brain') )
572
573       my $value = $record->{$_};
574
575       my $subq;
576
577       $subq = ($value ? 'EXISTS ' : 'NOT EXISTS ') .
578       "( SELECT part_virtual_field.name, virtual_field.value ".
579       "FROM part_virtual_field JOIN virtual_field ".
580       "ON part_virtual_field.vfieldpart = virtual_field.vfieldpart ".
581       "WHERE virtual_field.recnum = ${table}.${pkey} ".
582       "AND part_virtual_field.name = '${column}'".
583       ($value ? 
584         " AND virtual_field.value ${op} '${value}'"
585       : "") . ")";
586       $subq;
587
588     } @{ $virtual_fields } ) ;
589 }
590
591 sub get_real_fields {
592   my $table = shift;
593   my $record = shift;
594   my $real_fields = shift;
595
596    ## this huge map was previously inline, just broke it out to help read the qsearch method, should be optimized for readability
597       return ( 
598       map {
599
600       my $op = '=';
601       my $column = $_;
602       my $type = dbdef->table($table)->column($column)->type;
603       my $value = $record->{$column};
604       $value = $value->{'value'} if ref($value);
605       if ( ref($record->{$_}) ) {
606         $op = $record->{$_}{'op'} if $record->{$_}{'op'};
607         #$op = 'LIKE' if $op =~ /^ILIKE$/i && driver_name ne 'Pg';
608         if ( uc($op) eq 'ILIKE' ) {
609           $op = 'LIKE';
610           $record->{$_}{'value'} = lc($record->{$_}{'value'});
611           $column = "LOWER($_)";
612         }
613         $record->{$_} = $record->{$_}{'value'}
614       }
615
616       if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
617         if ( $op eq '=' ) {
618           if ( driver_name eq 'Pg' ) {
619             if ( $type =~ /(int|numeric|real|float4|(big)?serial)/i ) {
620               qq-( $column IS NULL )-;
621             } else {
622               qq-( $column IS NULL OR $column = '' )-;
623             }
624           } else {
625             qq-( $column IS NULL OR $column = "" )-;
626           }
627         } elsif ( $op eq '!=' ) {
628           if ( driver_name eq 'Pg' ) {
629             if ( $type =~ /(int|numeric|real|float4|(big)?serial)/i ) {
630               qq-( $column IS NOT NULL )-;
631             } else {
632               qq-( $column IS NOT NULL AND $column != '' )-;
633             }
634           } else {
635             qq-( $column IS NOT NULL AND $column != "" )-;
636           }
637         } else {
638           if ( driver_name eq 'Pg' ) {
639             qq-( $column $op '' )-;
640           } else {
641             qq-( $column $op "" )-;
642           }
643         }
644       #if this needs to be re-enabled, it needs to use a custom op like
645       #"APPROX=" or something (better name?, not '=', to avoid affecting other
646       # searches
647       #} elsif ( $op eq 'APPROX=' && _is_fs_float( $type, $value ) ) {
648       #  ( "$column <= ?", "$column >= ?" );
649       } else {
650         "$column $op ?";
651       }
652     } @{ $real_fields } );  
653 }
654
655 =item by_key PRIMARY_KEY_VALUE
656
657 This is a class method that returns the record with the given primary key
658 value.  This method is only useful in FS::Record subclasses.  For example:
659
660   my $cust_main = FS::cust_main->by_key(1); # retrieve customer with custnum 1
661
662 is equivalent to:
663
664   my $cust_main = qsearchs('cust_main', { 'custnum' => 1 } );
665
666 =cut
667
668 sub by_key {
669   my ($class, $pkey_value) = @_;
670
671   my $table = $class->table
672     or croak "No table for $class found";
673
674   my $dbdef_table = dbdef->table($table)
675     or die "No schema for table $table found - ".
676            "do you need to create it or run dbdef-create?";
677   my $pkey = $dbdef_table->primary_key
678     or die "No primary key for table $table";
679
680   return qsearchs($table, { $pkey => $pkey_value });
681 }
682
683 =item jsearch TABLE, HASHREF, SELECT, EXTRA_SQL, PRIMARY_TABLE, PRIMARY_KEY
684
685 Experimental JOINed search method.  Using this method, you can execute a
686 single SELECT spanning multiple tables, and cache the results for subsequent
687 method calls.  Interface will almost definately change in an incompatible
688 fashion.
689
690 Arguments: 
691
692 =cut
693
694 sub jsearch {
695   my($table, $record, $select, $extra_sql, $ptable, $pkey ) = @_;
696   my $cache = FS::SearchCache->new( $ptable, $pkey );
697   my %saw;
698   ( $cache,
699     grep { !$saw{$_->getfield($pkey)}++ }
700       qsearch($table, $record, $select, $extra_sql, $cache )
701   );
702 }
703
704 =item qsearchs PARAMS_HASHREF | TABLE, HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ, ADDL_FROM
705
706 Same as qsearch, except that if more than one record matches, it B<carp>s but
707 returns the first.  If this happens, you either made a logic error in asking
708 for a single item, or your data is corrupted.
709
710 =cut
711
712 sub qsearchs { # $result_record = &FS::Record:qsearchs('table',\%hash);
713   my $table = $_[0];
714   my(@result) = qsearch(@_);
715   cluck "warning: Multiple records in scalar search ($table)"
716     if scalar(@result) > 1;
717   #should warn more vehemently if the search was on a primary key?
718   scalar(@result) ? ($result[0]) : ();
719 }
720
721 =back
722
723 =head1 METHODS
724
725 =over 4
726
727 =item table
728
729 Returns the table name.
730
731 =cut
732
733 sub table {
734 #  cluck "warning: FS::Record::table deprecated; supply one in subclass!";
735   my $self = shift;
736   $self -> {'Table'};
737 }
738
739 =item dbdef_table
740
741 Returns the DBIx::DBSchema::Table object for the table.
742
743 =cut
744
745 sub dbdef_table {
746   my($self)=@_;
747   my($table)=$self->table;
748   dbdef->table($table);
749 }
750
751 =item primary_key
752
753 Returns the primary key for the table.
754
755 =cut
756
757 sub primary_key {
758   my $self = shift;
759   my $pkey = $self->dbdef_table->primary_key;
760 }
761
762 =item get, getfield COLUMN
763
764 Returns the value of the column/field/key COLUMN.
765
766 =cut
767
768 sub get {
769   my($self,$field) = @_;
770   # to avoid "Use of unitialized value" errors
771   if ( defined ( $self->{Hash}->{$field} ) ) {
772     $self->{Hash}->{$field};
773   } else { 
774     '';
775   }
776 }
777 sub getfield {
778   my $self = shift;
779   $self->get(@_);
780 }
781
782 =item set, setfield COLUMN, VALUE
783
784 Sets the value of the column/field/key COLUMN to VALUE.  Returns VALUE.
785
786 =cut
787
788 sub set { 
789   my($self,$field,$value) = @_;
790   $self->{'modified'} = 1;
791   $self->{'Hash'}->{$field} = $value;
792 }
793 sub setfield {
794   my $self = shift;
795   $self->set(@_);
796 }
797
798 =item AUTLOADED METHODS
799
800 $record->column is a synonym for $record->get('column');
801
802 $record->column('value') is a synonym for $record->set('column','value');
803
804 =cut
805
806 # readable/safe
807 sub AUTOLOAD {
808   my($self,$value)=@_;
809   my($field)=$AUTOLOAD;
810   $field =~ s/.*://;
811   if ( defined($value) ) {
812     confess "errant AUTOLOAD $field for $self (arg $value)"
813       unless blessed($self) && $self->can('setfield');
814     $self->setfield($field,$value);
815   } else {
816     confess "errant AUTOLOAD $field for $self (no args)"
817       unless blessed($self) && $self->can('getfield');
818     $self->getfield($field);
819   }    
820 }
821
822 # efficient
823 #sub AUTOLOAD {
824 #  my $field = $AUTOLOAD;
825 #  $field =~ s/.*://;
826 #  if ( defined($_[1]) ) {
827 #    $_[0]->setfield($field, $_[1]);
828 #  } else {
829 #    $_[0]->getfield($field);
830 #  }    
831 #}
832
833 =item hash
834
835 Returns a list of the column/value pairs, usually for assigning to a new hash.
836
837 To make a distinct duplicate of an FS::Record object, you can do:
838
839     $new = new FS::Record ( $old->table, { $old->hash } );
840
841 =cut
842
843 sub hash {
844   my($self) = @_;
845   confess $self. ' -> hash: Hash attribute is undefined'
846     unless defined($self->{'Hash'});
847   %{ $self->{'Hash'} }; 
848 }
849
850 =item hashref
851
852 Returns a reference to the column/value hash.  This may be deprecated in the
853 future; if there's a reason you can't just use the autoloaded or get/set
854 methods, speak up.
855
856 =cut
857
858 sub hashref {
859   my($self) = @_;
860   $self->{'Hash'};
861 }
862
863 =item modified
864
865 Returns true if any of this object's values have been modified with set (or via
866 an autoloaded method).  Doesn't yet recognize when you retreive a hashref and
867 modify that.
868
869 =cut
870
871 sub modified {
872   my $self = shift;
873   $self->{'modified'};
874 }
875
876 =item select_for_update
877
878 Selects this record with the SQL "FOR UPDATE" command.  This can be useful as
879 a mutex.
880
881 =cut
882
883 sub select_for_update {
884   my $self = shift;
885   my $primary_key = $self->primary_key;
886   qsearchs( {
887     'select'    => '*',
888     'table'     => $self->table,
889     'hashref'   => { $primary_key => $self->$primary_key() },
890     'extra_sql' => 'FOR UPDATE',
891   } );
892 }
893
894 =item lock_table
895
896 Locks this table with a database-driver specific lock method.  This is used
897 as a mutex in order to do a duplicate search.
898
899 For PostgreSQL, does "LOCK TABLE tablename IN SHARE ROW EXCLUSIVE MODE".
900
901 For MySQL, does a SELECT FOR UPDATE on the duplicate_lock table.
902
903 Errors are fatal; no useful return value.
904
905 Note: To use this method for new tables other than svc_acct and svc_phone,
906 edit freeside-upgrade and add those tables to the duplicate_lock list.
907
908 =cut
909
910 sub lock_table {
911   my $self = shift;
912   my $table = $self->table;
913
914   warn "$me locking $table table\n" if $DEBUG;
915
916   if ( driver_name =~ /^Pg/i ) {
917
918     dbh->do("LOCK TABLE $table IN SHARE ROW EXCLUSIVE MODE")
919       or die dbh->errstr;
920
921   } elsif ( driver_name =~ /^mysql/i ) {
922
923     dbh->do("SELECT * FROM duplicate_lock
924                WHERE lockname = '$table'
925                FOR UPDATE"
926            ) or die dbh->errstr;
927
928   } else {
929
930     die "unknown database ". driver_name. "; don't know how to lock table";
931
932   }
933
934   warn "$me acquired $table table lock\n" if $DEBUG;
935
936 }
937
938 =item insert
939
940 Inserts this record to the database.  If there is an error, returns the error,
941 otherwise returns false.
942
943 =cut
944
945 sub insert {
946   my $self = shift;
947   my $saved = {};
948
949   warn "$self -> insert" if $DEBUG;
950
951   my $error = $self->check;
952   return $error if $error;
953
954   #single-field unique keys are given a value if false
955   #(like MySQL's AUTO_INCREMENT or Pg SERIAL)
956   foreach ( $self->dbdef_table->unique_singles) {
957     $self->unique($_) unless $self->getfield($_);
958   }
959
960   #and also the primary key, if the database isn't going to
961   my $primary_key = $self->dbdef_table->primary_key;
962   my $db_seq = 0;
963   if ( $primary_key ) {
964     my $col = $self->dbdef_table->column($primary_key);
965     
966     $db_seq =
967       uc($col->type) =~ /^(BIG)?SERIAL\d?/
968       || ( driver_name eq 'Pg'
969              && defined($col->default)
970              && $col->default =~ /^nextval\(/i
971          )
972       || ( driver_name eq 'mysql'
973              && defined($col->local)
974              && $col->local =~ /AUTO_INCREMENT/i
975          );
976     $self->unique($primary_key) unless $self->getfield($primary_key) || $db_seq;
977   }
978
979   my $table = $self->table;
980   
981   # Encrypt before the database
982   if (    defined(eval '@FS::'. $table . '::encrypted_fields')
983        && scalar( eval '@FS::'. $table . '::encrypted_fields')
984        && $conf->exists('encryption')
985   ) {
986     foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
987       $self->{'saved'} = $self->getfield($field);
988       $self->setfield($field, $self->encrypt($self->getfield($field)));
989     }
990   }
991
992   #false laziness w/delete
993   my @real_fields =
994     grep { defined($self->getfield($_)) && $self->getfield($_) ne "" }
995     real_fields($table)
996   ;
997   my @values = map { _quote( $self->getfield($_), $table, $_) } @real_fields;
998   #eslaf
999
1000   my $statement = "INSERT INTO $table ";
1001   if ( @real_fields ) {
1002     $statement .=
1003       "( ".
1004         join( ', ', @real_fields ).
1005       ") VALUES (".
1006         join( ', ', @values ).
1007        ")"
1008     ;
1009   } else {
1010     $statement .= 'DEFAULT VALUES';
1011   }
1012   warn "[debug]$me $statement\n" if $DEBUG > 1;
1013   my $sth = dbh->prepare($statement) or return dbh->errstr;
1014
1015   local $SIG{HUP} = 'IGNORE';
1016   local $SIG{INT} = 'IGNORE';
1017   local $SIG{QUIT} = 'IGNORE'; 
1018   local $SIG{TERM} = 'IGNORE';
1019   local $SIG{TSTP} = 'IGNORE';
1020   local $SIG{PIPE} = 'IGNORE';
1021
1022   $sth->execute or return $sth->errstr;
1023
1024   # get inserted id from the database, if applicable & needed
1025   if ( $db_seq && ! $self->getfield($primary_key) ) {
1026     warn "[debug]$me retreiving sequence from database\n" if $DEBUG;
1027   
1028     my $insertid = '';
1029
1030     if ( driver_name eq 'Pg' ) {
1031
1032       #my $oid = $sth->{'pg_oid_status'};
1033       #my $i_sql = "SELECT $primary_key FROM $table WHERE oid = ?";
1034
1035       my $default = $self->dbdef_table->column($primary_key)->default;
1036       unless ( $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i ) {
1037         dbh->rollback if $FS::UID::AutoCommit;
1038         return "can't parse $table.$primary_key default value".
1039                " for sequence name: $default";
1040       }
1041       my $sequence = $1;
1042
1043       my $i_sql = "SELECT currval('$sequence')";
1044       my $i_sth = dbh->prepare($i_sql) or do {
1045         dbh->rollback if $FS::UID::AutoCommit;
1046         return dbh->errstr;
1047       };
1048       $i_sth->execute() or do { #$i_sth->execute($oid)
1049         dbh->rollback if $FS::UID::AutoCommit;
1050         return $i_sth->errstr;
1051       };
1052       $insertid = $i_sth->fetchrow_arrayref->[0];
1053
1054     } elsif ( driver_name eq 'mysql' ) {
1055
1056       $insertid = dbh->{'mysql_insertid'};
1057       # work around mysql_insertid being null some of the time, ala RT :/
1058       unless ( $insertid ) {
1059         warn "WARNING: DBD::mysql didn't return mysql_insertid; ".
1060              "using SELECT LAST_INSERT_ID();";
1061         my $i_sql = "SELECT LAST_INSERT_ID()";
1062         my $i_sth = dbh->prepare($i_sql) or do {
1063           dbh->rollback if $FS::UID::AutoCommit;
1064           return dbh->errstr;
1065         };
1066         $i_sth->execute or do {
1067           dbh->rollback if $FS::UID::AutoCommit;
1068           return $i_sth->errstr;
1069         };
1070         $insertid = $i_sth->fetchrow_arrayref->[0];
1071       }
1072
1073     } else {
1074
1075       dbh->rollback if $FS::UID::AutoCommit;
1076       return "don't know how to retreive inserted ids from ". driver_name. 
1077              ", try using counterfiles (maybe run dbdef-create?)";
1078
1079     }
1080
1081     $self->setfield($primary_key, $insertid);
1082
1083   }
1084
1085   my @virtual_fields = 
1086       grep defined($self->getfield($_)) && $self->getfield($_) ne "",
1087           $self->virtual_fields;
1088   if (@virtual_fields) {
1089     my %v_values = map { $_, $self->getfield($_) } @virtual_fields;
1090
1091     my $vfieldpart = $self->vfieldpart_hashref;
1092
1093     my $v_statement = "INSERT INTO virtual_field(recnum, vfieldpart, value) ".
1094                     "VALUES (?, ?, ?)";
1095
1096     my $v_sth = dbh->prepare($v_statement) or do {
1097       dbh->rollback if $FS::UID::AutoCommit;
1098       return dbh->errstr;
1099     };
1100
1101     foreach (keys(%v_values)) {
1102       $v_sth->execute($self->getfield($primary_key),
1103                       $vfieldpart->{$_},
1104                       $v_values{$_})
1105       or do {
1106         dbh->rollback if $FS::UID::AutoCommit;
1107         return $v_sth->errstr;
1108       };
1109     }
1110   }
1111
1112
1113   my $h_sth;
1114   if ( defined dbdef->table('h_'. $table) ) {
1115     my $h_statement = $self->_h_statement('insert');
1116     warn "[debug]$me $h_statement\n" if $DEBUG > 2;
1117     $h_sth = dbh->prepare($h_statement) or do {
1118       dbh->rollback if $FS::UID::AutoCommit;
1119       return dbh->errstr;
1120     };
1121   } else {
1122     $h_sth = '';
1123   }
1124   $h_sth->execute or return $h_sth->errstr if $h_sth;
1125
1126   dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
1127
1128   # Now that it has been saved, reset the encrypted fields so that $new 
1129   # can still be used.
1130   foreach my $field (keys %{$saved}) {
1131     $self->setfield($field, $saved->{$field});
1132   }
1133
1134   '';
1135 }
1136
1137 =item add
1138
1139 Depriciated (use insert instead).
1140
1141 =cut
1142
1143 sub add {
1144   cluck "warning: FS::Record::add deprecated!";
1145   insert @_; #call method in this scope
1146 }
1147
1148 =item delete
1149
1150 Delete this record from the database.  If there is an error, returns the error,
1151 otherwise returns false.
1152
1153 =cut
1154
1155 sub delete {
1156   my $self = shift;
1157
1158   my $statement = "DELETE FROM ". $self->table. " WHERE ". join(' AND ',
1159     map {
1160       $self->getfield($_) eq ''
1161         #? "( $_ IS NULL OR $_ = \"\" )"
1162         ? ( driver_name eq 'Pg'
1163               ? "$_ IS NULL"
1164               : "( $_ IS NULL OR $_ = \"\" )"
1165           )
1166         : "$_ = ". _quote($self->getfield($_),$self->table,$_)
1167     } ( $self->dbdef_table->primary_key )
1168           ? ( $self->dbdef_table->primary_key)
1169           : real_fields($self->table)
1170   );
1171   warn "[debug]$me $statement\n" if $DEBUG > 1;
1172   my $sth = dbh->prepare($statement) or return dbh->errstr;
1173
1174   my $h_sth;
1175   if ( defined dbdef->table('h_'. $self->table) ) {
1176     my $h_statement = $self->_h_statement('delete');
1177     warn "[debug]$me $h_statement\n" if $DEBUG > 2;
1178     $h_sth = dbh->prepare($h_statement) or return dbh->errstr;
1179   } else {
1180     $h_sth = '';
1181   }
1182
1183   my $primary_key = $self->dbdef_table->primary_key;
1184   my $v_sth;
1185   my @del_vfields;
1186   my $vfp = $self->vfieldpart_hashref;
1187   foreach($self->virtual_fields) {
1188     next if $self->getfield($_) eq '';
1189     unless(@del_vfields) {
1190       my $st = "DELETE FROM virtual_field WHERE recnum = ? AND vfieldpart = ?";
1191       $v_sth = dbh->prepare($st) or return dbh->errstr;
1192     }
1193     push @del_vfields, $_;
1194   }
1195
1196   local $SIG{HUP} = 'IGNORE';
1197   local $SIG{INT} = 'IGNORE';
1198   local $SIG{QUIT} = 'IGNORE'; 
1199   local $SIG{TERM} = 'IGNORE';
1200   local $SIG{TSTP} = 'IGNORE';
1201   local $SIG{PIPE} = 'IGNORE';
1202
1203   my $rc = $sth->execute or return $sth->errstr;
1204   #not portable #return "Record not found, statement:\n$statement" if $rc eq "0E0";
1205   $h_sth->execute or return $h_sth->errstr if $h_sth;
1206   $v_sth->execute($self->getfield($primary_key), $vfp->{$_}) 
1207     or return $v_sth->errstr 
1208         foreach (@del_vfields);
1209   
1210   dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
1211
1212   #no need to needlessly destoy the data either (causes problems actually)
1213   #undef $self; #no need to keep object!
1214
1215   '';
1216 }
1217
1218 =item del
1219
1220 Depriciated (use delete instead).
1221
1222 =cut
1223
1224 sub del {
1225   cluck "warning: FS::Record::del deprecated!";
1226   &delete(@_); #call method in this scope
1227 }
1228
1229 =item replace OLD_RECORD
1230
1231 Replace the OLD_RECORD with this one in the database.  If there is an error,
1232 returns the error, otherwise returns false.
1233
1234 =cut
1235
1236 sub replace {
1237   my ($new, $old) = (shift, shift);
1238
1239   $old = $new->replace_old unless defined($old);
1240
1241   warn "[debug]$me $new ->replace $old\n" if $DEBUG;
1242
1243   if ( $new->can('replace_check') ) {
1244     my $error = $new->replace_check($old);
1245     return $error if $error;
1246   }
1247
1248   return "Records not in same table!" unless $new->table eq $old->table;
1249
1250   my $primary_key = $old->dbdef_table->primary_key;
1251   return "Can't change primary key $primary_key ".
1252          'from '. $old->getfield($primary_key).
1253          ' to ' . $new->getfield($primary_key)
1254     if $primary_key
1255        && ( $old->getfield($primary_key) ne $new->getfield($primary_key) );
1256
1257   my $error = $new->check;
1258   return $error if $error;
1259   
1260   # Encrypt for replace
1261   my $saved = {};
1262   if (    $conf->exists('encryption')
1263        && defined(eval '@FS::'. $new->table . '::encrypted_fields')
1264        && scalar( eval '@FS::'. $new->table . '::encrypted_fields')
1265   ) {
1266     foreach my $field (eval '@FS::'. $new->table . '::encrypted_fields') {
1267       $saved->{$field} = $new->getfield($field);
1268       $new->setfield($field, $new->encrypt($new->getfield($field)));
1269     }
1270   }
1271
1272   #my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields;
1273   my %diff = map { ($new->getfield($_) ne $old->getfield($_))
1274                    ? ($_, $new->getfield($_)) : () } $old->fields;
1275                    
1276   unless (keys(%diff) || $no_update_diff ) {
1277     carp "[warning]$me $new -> replace $old: records identical"
1278       unless $nowarn_identical;
1279     return '';
1280   }
1281
1282   my $statement = "UPDATE ". $old->table. " SET ". join(', ',
1283     map {
1284       "$_ = ". _quote($new->getfield($_),$old->table,$_) 
1285     } real_fields($old->table)
1286   ). ' WHERE '.
1287     join(' AND ',
1288       map {
1289
1290         if ( $old->getfield($_) eq '' ) {
1291
1292          #false laziness w/qsearch
1293          if ( driver_name eq 'Pg' ) {
1294             my $type = $old->dbdef_table->column($_)->type;
1295             if ( $type =~ /(int|(big)?serial)/i ) {
1296               qq-( $_ IS NULL )-;
1297             } else {
1298               qq-( $_ IS NULL OR $_ = '' )-;
1299             }
1300           } else {
1301             qq-( $_ IS NULL OR $_ = "" )-;
1302           }
1303
1304         } else {
1305           "$_ = ". _quote($old->getfield($_),$old->table,$_);
1306         }
1307
1308       } ( $primary_key ? ( $primary_key ) : real_fields($old->table) )
1309     )
1310   ;
1311   warn "[debug]$me $statement\n" if $DEBUG > 1;
1312   my $sth = dbh->prepare($statement) or return dbh->errstr;
1313
1314   my $h_old_sth;
1315   if ( defined dbdef->table('h_'. $old->table) ) {
1316     my $h_old_statement = $old->_h_statement('replace_old');
1317     warn "[debug]$me $h_old_statement\n" if $DEBUG > 2;
1318     $h_old_sth = dbh->prepare($h_old_statement) or return dbh->errstr;
1319   } else {
1320     $h_old_sth = '';
1321   }
1322
1323   my $h_new_sth;
1324   if ( defined dbdef->table('h_'. $new->table) ) {
1325     my $h_new_statement = $new->_h_statement('replace_new');
1326     warn "[debug]$me $h_new_statement\n" if $DEBUG > 2;
1327     $h_new_sth = dbh->prepare($h_new_statement) or return dbh->errstr;
1328   } else {
1329     $h_new_sth = '';
1330   }
1331
1332   # For virtual fields we have three cases with different SQL 
1333   # statements: add, replace, delete
1334   my $v_add_sth;
1335   my $v_rep_sth;
1336   my $v_del_sth;
1337   my (@add_vfields, @rep_vfields, @del_vfields);
1338   my $vfp = $old->vfieldpart_hashref;
1339   foreach(grep { exists($diff{$_}) } $new->virtual_fields) {
1340     if($diff{$_} eq '') {
1341       # Delete
1342       unless(@del_vfields) {
1343         my $st = "DELETE FROM virtual_field WHERE recnum = ? ".
1344                  "AND vfieldpart = ?";
1345         warn "[debug]$me $st\n" if $DEBUG > 2;
1346         $v_del_sth = dbh->prepare($st) or return dbh->errstr;
1347       }
1348       push @del_vfields, $_;
1349     } elsif($old->getfield($_) eq '') {
1350       # Add
1351       unless(@add_vfields) {
1352         my $st = "INSERT INTO virtual_field (value, recnum, vfieldpart) ".
1353                  "VALUES (?, ?, ?)";
1354         warn "[debug]$me $st\n" if $DEBUG > 2;
1355         $v_add_sth = dbh->prepare($st) or return dbh->errstr;
1356       }
1357       push @add_vfields, $_;
1358     } else {
1359       # Replace
1360       unless(@rep_vfields) {
1361         my $st = "UPDATE virtual_field SET value = ? ".
1362                  "WHERE recnum = ? AND vfieldpart = ?";
1363         warn "[debug]$me $st\n" if $DEBUG > 2;
1364         $v_rep_sth = dbh->prepare($st) or return dbh->errstr;
1365       }
1366       push @rep_vfields, $_;
1367     }
1368   }
1369
1370   local $SIG{HUP} = 'IGNORE';
1371   local $SIG{INT} = 'IGNORE';
1372   local $SIG{QUIT} = 'IGNORE'; 
1373   local $SIG{TERM} = 'IGNORE';
1374   local $SIG{TSTP} = 'IGNORE';
1375   local $SIG{PIPE} = 'IGNORE';
1376
1377   my $rc = $sth->execute or return $sth->errstr;
1378   #not portable #return "Record not found (or records identical)." if $rc eq "0E0";
1379   $h_old_sth->execute or return $h_old_sth->errstr if $h_old_sth;
1380   $h_new_sth->execute or return $h_new_sth->errstr if $h_new_sth;
1381
1382   $v_del_sth->execute($old->getfield($primary_key),
1383                       $vfp->{$_})
1384         or return $v_del_sth->errstr
1385       foreach(@del_vfields);
1386
1387   $v_add_sth->execute($new->getfield($_),
1388                       $old->getfield($primary_key),
1389                       $vfp->{$_})
1390         or return $v_add_sth->errstr
1391       foreach(@add_vfields);
1392
1393   $v_rep_sth->execute($new->getfield($_),
1394                       $old->getfield($primary_key),
1395                       $vfp->{$_})
1396         or return $v_rep_sth->errstr
1397       foreach(@rep_vfields);
1398
1399   dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
1400
1401   # Now that it has been saved, reset the encrypted fields so that $new 
1402   # can still be used.
1403   foreach my $field (keys %{$saved}) {
1404     $new->setfield($field, $saved->{$field});
1405   }
1406
1407   '';
1408
1409 }
1410
1411 sub replace_old {
1412   my( $self ) = shift;
1413   warn "[$me] replace called with no arguments; autoloading old record\n"
1414     if $DEBUG;
1415
1416   my $primary_key = $self->dbdef_table->primary_key;
1417   if ( $primary_key ) {
1418     $self->by_key( $self->$primary_key() ) #this is what's returned
1419       or croak "can't find ". $self->table. ".$primary_key ".
1420         $self->$primary_key();
1421   } else {
1422     croak $self->table. " has no primary key; pass old record as argument";
1423   }
1424
1425 }
1426
1427 =item rep
1428
1429 Depriciated (use replace instead).
1430
1431 =cut
1432
1433 sub rep {
1434   cluck "warning: FS::Record::rep deprecated!";
1435   replace @_; #call method in this scope
1436 }
1437
1438 =item check
1439
1440 Checks virtual fields (using check_blocks).  Subclasses should still provide 
1441 a check method to validate real fields, foreign keys, etc., and call this 
1442 method via $self->SUPER::check.
1443
1444 (FIXME: Should this method try to make sure that it I<is> being called from 
1445 a subclass's check method, to keep the current semantics as far as possible?)
1446
1447 =cut
1448
1449 sub check {
1450   #confess "FS::Record::check not implemented; supply one in subclass!";
1451   my $self = shift;
1452
1453   foreach my $field ($self->virtual_fields) {
1454     for ($self->getfield($field)) {
1455       # See notes on check_block in FS::part_virtual_field.
1456       eval $self->pvf($field)->check_block;
1457       if ( $@ ) {
1458         #this is bad, probably want to follow the stack backtrace up and see
1459         #wtf happened
1460         my $err = "Fatal error checking $field for $self";
1461         cluck "$err: $@";
1462         return "$err (see log for backtrace): $@";
1463
1464       }
1465       $self->setfield($field, $_);
1466     }
1467   }
1468   '';
1469 }
1470
1471 =item process_batch_import JOB OPTIONS_HASHREF PARAMS
1472
1473 Processes a batch import as a queued JSRPC job
1474
1475 JOB is an FS::queue entry.
1476
1477 OPTIONS_HASHREF can have the following keys:
1478
1479 =over 4
1480
1481 =item table
1482
1483 Table name (required).
1484
1485 =item params
1486
1487 Listref of field names for static fields.  They will be given values from the
1488 PARAMS hashref and passed as a "params" hashref to batch_import.
1489
1490 =item formats
1491
1492 Formats hashref.  Keys are field names, values are listrefs that define the
1493 format.
1494
1495 Each listref value can be a column name or a code reference.  Coderefs are run
1496 with the row object, data and a FS::Conf object as the three parameters.
1497 For example, this coderef does the same thing as using the "columnname" string:
1498
1499   sub {
1500     my( $record, $data, $conf ) = @_;
1501     $record->columnname( $data );
1502   },
1503
1504 Coderefs are run after all "column name" fields are assigned.
1505
1506 =item format_types
1507
1508 Optional format hashref of types.  Keys are field names, values are "csv",
1509 "xls" or "fixedlength".  Overrides automatic determination of file type
1510 from extension.
1511
1512 =item format_headers
1513
1514 Optional format hashref of header lines.  Keys are field names, values are 0
1515 for no header, 1 to ignore the first line, or to higher numbers to ignore that
1516 number of lines.
1517
1518 =item format_sep_chars
1519
1520 Optional format hashref of CSV sep_chars.  Keys are field names, values are the
1521 CSV separation character.
1522
1523 =item format_fixedlenth_formats
1524
1525 Optional format hashref of fixed length format defintiions.  Keys are field
1526 names, values Parse::FixedLength listrefs of field definitions.
1527
1528 =item default_csv
1529
1530 Set true to default to CSV file type if the filename does not contain a
1531 recognizable ".csv" or ".xls" extension (and type is not pre-specified by
1532 format_types).
1533
1534 =back
1535
1536 PARAMS is a base64-encoded Storable string containing the POSTed data as
1537 a hash ref.  It normally contains at least one field, "uploaded files",
1538 generated by /elements/file-upload.html and containing the list of uploaded
1539 files.  Currently only supports a single file named "file".
1540
1541 =cut
1542
1543 use Storable qw(thaw);
1544 use Data::Dumper;
1545 use MIME::Base64;
1546 sub process_batch_import {
1547   my($job, $opt) = ( shift, shift );
1548
1549   my $table = $opt->{table};
1550   my @pass_params = @{ $opt->{params} };
1551   my %formats = %{ $opt->{formats} };
1552
1553   my $param = thaw(decode_base64(shift));
1554   warn Dumper($param) if $DEBUG;
1555   
1556   my $files = $param->{'uploaded_files'}
1557     or die "No files provided.\n";
1558
1559   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
1560
1561   my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
1562   my $file = $dir. $files{'file'};
1563
1564   my $error =
1565     FS::Record::batch_import( {
1566       #class-static
1567       table                      => $table,
1568       formats                    => \%formats,
1569       format_types               => $opt->{format_types},
1570       format_headers             => $opt->{format_headers},
1571       format_sep_chars           => $opt->{format_sep_chars},
1572       format_fixedlength_formats => $opt->{format_fixedlength_formats},
1573       #per-import
1574       job                        => $job,
1575       file                       => $file,
1576       #type                       => $type,
1577       format                     => $param->{format},
1578       params                     => { map { $_ => $param->{$_} } @pass_params },
1579       #?
1580       default_csv                => $opt->{default_csv},
1581     } );
1582
1583   unlink $file;
1584
1585   die "$error\n" if $error;
1586 }
1587
1588 =item batch_import PARAM_HASHREF
1589
1590 Class method for batch imports.  Available params:
1591
1592 =over 4
1593
1594 =item table
1595
1596 =item formats
1597
1598 =item format_types
1599
1600 =item format_headers
1601
1602 =item format_sep_chars
1603
1604 =item format_fixedlength_formats
1605
1606 =item params
1607
1608 =item job
1609
1610 FS::queue object, will be updated with progress
1611
1612 =item file
1613
1614 =item type
1615
1616 csv, xls or fixedlength
1617
1618 =item format
1619
1620 =item empty_ok
1621
1622 =back
1623
1624 =cut
1625
1626 sub batch_import {
1627   my $param = shift;
1628
1629   warn "$me batch_import call with params: \n". Dumper($param)
1630     if $DEBUG;
1631
1632   my $table   = $param->{table};
1633   my $formats = $param->{formats};
1634
1635   my $job     = $param->{job};
1636   my $file    = $param->{file};
1637   my $format  = $param->{'format'};
1638   my $params  = $param->{params} || {};
1639
1640   die "unknown format $format" unless exists $formats->{ $format };
1641
1642   my $type = $param->{'format_types'}
1643              ? $param->{'format_types'}{ $format }
1644              : $param->{type} || 'csv';
1645
1646   unless ( $type ) {
1647     if ( $file =~ /\.(\w+)$/i ) {
1648       $type = lc($1);
1649     } else {
1650       #or error out???
1651       warn "can't parse file type from filename $file; defaulting to CSV";
1652       $type = 'csv';
1653     }
1654     $type = 'csv'
1655       if $param->{'default_csv'} && $type ne 'xls';
1656   }
1657
1658   my $header = $param->{'format_headers'}
1659                  ? $param->{'format_headers'}{ $param->{'format'} }
1660                  : 0;
1661
1662   my $sep_char = $param->{'format_sep_chars'}
1663                    ? $param->{'format_sep_chars'}{ $param->{'format'} }
1664                    : ',';
1665
1666   my $fixedlength_format =
1667     $param->{'format_fixedlength_formats'}
1668       ? $param->{'format_fixedlength_formats'}{ $param->{'format'} }
1669       : '';
1670
1671   my @fields = @{ $formats->{ $format } };
1672
1673   my $row = 0;
1674   my $count;
1675   my $parser;
1676   my @buffer = ();
1677   if ( $type eq 'csv' || $type eq 'fixedlength' ) {
1678
1679     if ( $type eq 'csv' ) {
1680
1681       my %attr = ();
1682       $attr{sep_char} = $sep_char if $sep_char;
1683       $parser = new Text::CSV_XS \%attr;
1684
1685     } elsif ( $type eq 'fixedlength' ) {
1686
1687       eval "use Parse::FixedLength;";
1688       die $@ if $@;
1689       $parser = new Parse::FixedLength $fixedlength_format;
1690  
1691     } else {
1692       die "Unknown file type $type\n";
1693     }
1694
1695     @buffer = split(/\r?\n/, slurp($file) );
1696     splice(@buffer, 0, ($header || 0) );
1697     $count = scalar(@buffer);
1698
1699   } elsif ( $type eq 'xls' ) {
1700
1701     eval "use Spreadsheet::ParseExcel;";
1702     die $@ if $@;
1703
1704     eval "use DateTime::Format::Excel;";
1705     #for now, just let the error be thrown if it is used, since only CDR
1706     # formats bill_west and troop use it, not other excel-parsing things
1707     #die $@ if $@;
1708
1709     my $excel = Spreadsheet::ParseExcel::Workbook->new->Parse($file);
1710
1711     $parser = $excel->{Worksheet}[0]; #first sheet
1712
1713     $count = $parser->{MaxRow} || $parser->{MinRow};
1714     $count++;
1715
1716     $row = $header || 0;
1717
1718   } else {
1719     die "Unknown file type $type\n";
1720   }
1721
1722   #my $columns;
1723
1724   local $SIG{HUP} = 'IGNORE';
1725   local $SIG{INT} = 'IGNORE';
1726   local $SIG{QUIT} = 'IGNORE';
1727   local $SIG{TERM} = 'IGNORE';
1728   local $SIG{TSTP} = 'IGNORE';
1729   local $SIG{PIPE} = 'IGNORE';
1730
1731   my $oldAutoCommit = $FS::UID::AutoCommit;
1732   local $FS::UID::AutoCommit = 0;
1733   my $dbh = dbh;
1734   
1735   my $line;
1736   my $imported = 0;
1737   my( $last, $min_sec ) = ( time, 5 ); #progressbar foo
1738   while (1) {
1739
1740     my @columns = ();
1741     if ( $type eq 'csv' ) {
1742
1743       last unless scalar(@buffer);
1744       $line = shift(@buffer);
1745
1746       $parser->parse($line) or do {
1747         $dbh->rollback if $oldAutoCommit;
1748         return "can't parse: ". $parser->error_input();
1749       };
1750       @columns = $parser->fields();
1751
1752     } elsif ( $type eq 'fixedlength' ) {
1753
1754       @columns = $parser->parse($line);
1755
1756     } elsif ( $type eq 'xls' ) {
1757
1758       last if $row > ($parser->{MaxRow} || $parser->{MinRow})
1759            || ! $parser->{Cells}[$row];
1760
1761       my @row = @{ $parser->{Cells}[$row] };
1762       @columns = map $_->{Val}, @row;
1763
1764       #my $z = 'A';
1765       #warn $z++. ": $_\n" for @columns;
1766
1767     } else {
1768       die "Unknown file type $type\n";
1769     }
1770
1771     my @later = ();
1772     my %hash = %$params;
1773
1774     foreach my $field ( @fields ) {
1775
1776       my $value = shift @columns;
1777      
1778       if ( ref($field) eq 'CODE' ) {
1779         #&{$field}(\%hash, $value);
1780         push @later, $field, $value;
1781       } else {
1782         #??? $hash{$field} = $value if length($value);
1783         $hash{$field} = $value if defined($value) && length($value);
1784       }
1785
1786     }
1787
1788     my $class = "FS::$table";
1789
1790     my $record = $class->new( \%hash );
1791
1792     my $param = {};
1793     while ( scalar(@later) ) {
1794       my $sub = shift @later;
1795       my $data = shift @later;
1796       &{$sub}($record, $data, $conf, $param); # $record->&{$sub}($data, $conf);
1797       last if exists( $param->{skiprow} );
1798     }
1799     next if exists( $param->{skiprow} );
1800
1801     my $error = $record->insert;
1802
1803     if ( $error ) {
1804       $dbh->rollback if $oldAutoCommit;
1805       return "can't insert record". ( $line ? " for $line" : '' ). ": $error";
1806     }
1807
1808     $row++;
1809     $imported++;
1810
1811     if ( $job && time - $min_sec > $last ) { #progress bar
1812       $job->update_statustext( int(100 * $imported / $count) );
1813       $last = time;
1814     }
1815
1816   }
1817
1818   $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
1819
1820   return "Empty file!" unless $imported || $param->{empty_ok};
1821
1822   ''; #no error
1823
1824 }
1825
1826 sub _h_statement {
1827   my( $self, $action, $time ) = @_;
1828
1829   $time ||= time;
1830
1831   my %nohistory = map { $_=>1 } $self->nohistory_fields;
1832
1833   my @fields =
1834     grep { defined($self->get($_)) && $self->get($_) ne "" && ! $nohistory{$_} }
1835     real_fields($self->table);
1836   ;
1837
1838   # If we're encrypting then don't store the payinfo in the history
1839   if ( $conf && $conf->exists('encryption') ) {
1840     @fields = grep { $_ ne 'payinfo' } @fields;
1841   }
1842
1843   my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields;
1844
1845   "INSERT INTO h_". $self->table. " ( ".
1846       join(', ', qw(history_date history_user history_action), @fields ).
1847     ") VALUES (".
1848       join(', ', $time, dbh->quote(getotaker()), dbh->quote($action), @values).
1849     ")"
1850   ;
1851 }
1852
1853 =item unique COLUMN
1854
1855 B<Warning>: External use is B<deprecated>.  
1856
1857 Replaces COLUMN in record with a unique number, using counters in the
1858 filesystem.  Used by the B<insert> method on single-field unique columns
1859 (see L<DBIx::DBSchema::Table>) and also as a fallback for primary keys
1860 that aren't SERIAL (Pg) or AUTO_INCREMENT (mysql).
1861
1862 Returns the new value.
1863
1864 =cut
1865
1866 sub unique {
1867   my($self,$field) = @_;
1868   my($table)=$self->table;
1869
1870   croak "Unique called on field $field, but it is ",
1871         $self->getfield($field),
1872         ", not null!"
1873     if $self->getfield($field);
1874
1875   #warn "table $table is tainted" if is_tainted($table);
1876   #warn "field $field is tainted" if is_tainted($field);
1877
1878   my($counter) = new File::CounterFile "$table.$field",0;
1879 # hack for web demo
1880 #  getotaker() =~ /^([\w\-]{1,16})$/ or die "Illegal CGI REMOTE_USER!";
1881 #  my($user)=$1;
1882 #  my($counter) = new File::CounterFile "$user/$table.$field",0;
1883 # endhack
1884
1885   my $index = $counter->inc;
1886   $index = $counter->inc while qsearchs($table, { $field=>$index } );
1887
1888   $index =~ /^(\d*)$/;
1889   $index=$1;
1890
1891   $self->setfield($field,$index);
1892
1893 }
1894
1895 =item ut_float COLUMN
1896
1897 Check/untaint floating point numeric data: 1.1, 1, 1.1e10, 1e10.  May not be
1898 null.  If there is an error, returns the error, otherwise returns false.
1899
1900 =cut
1901
1902 sub ut_float {
1903   my($self,$field)=@_ ;
1904   ($self->getfield($field) =~ /^\s*(\d+\.\d+)\s*$/ ||
1905    $self->getfield($field) =~ /^\s*(\d+)\s*$/ ||
1906    $self->getfield($field) =~ /^\s*(\d+\.\d+e\d+)\s*$/ ||
1907    $self->getfield($field) =~ /^\s*(\d+e\d+)\s*$/)
1908     or return "Illegal or empty (float) $field: ". $self->getfield($field);
1909   $self->setfield($field,$1);
1910   '';
1911 }
1912 =item ut_floatn COLUMN
1913
1914 Check/untaint floating point numeric data: 1.1, 1, 1.1e10, 1e10.  May be
1915 null.  If there is an error, returns the error, otherwise returns false.
1916
1917 =cut
1918
1919 #false laziness w/ut_ipn
1920 sub ut_floatn {
1921   my( $self, $field ) = @_;
1922   if ( $self->getfield($field) =~ /^()$/ ) {
1923     $self->setfield($field,'');
1924     '';
1925   } else {
1926     $self->ut_float($field);
1927   }
1928 }
1929
1930 =item ut_sfloat COLUMN
1931
1932 Check/untaint signed floating point numeric data: 1.1, 1, 1.1e10, 1e10.
1933 May not be null.  If there is an error, returns the error, otherwise returns
1934 false.
1935
1936 =cut
1937
1938 sub ut_sfloat {
1939   my($self,$field)=@_ ;
1940   ($self->getfield($field) =~ /^\s*(-?\d+\.\d+)\s*$/ ||
1941    $self->getfield($field) =~ /^\s*(-?\d+)\s*$/ ||
1942    $self->getfield($field) =~ /^\s*(-?\d+\.\d+[eE]-?\d+)\s*$/ ||
1943    $self->getfield($field) =~ /^\s*(-?\d+[eE]-?\d+)\s*$/)
1944     or return "Illegal or empty (float) $field: ". $self->getfield($field);
1945   $self->setfield($field,$1);
1946   '';
1947 }
1948 =item ut_sfloatn COLUMN
1949
1950 Check/untaint signed floating point numeric data: 1.1, 1, 1.1e10, 1e10.  May be
1951 null.  If there is an error, returns the error, otherwise returns false.
1952
1953 =cut
1954
1955 sub ut_sfloatn {
1956   my( $self, $field ) = @_;
1957   if ( $self->getfield($field) =~ /^()$/ ) {
1958     $self->setfield($field,'');
1959     '';
1960   } else {
1961     $self->ut_sfloat($field);
1962   }
1963 }
1964
1965 =item ut_snumber COLUMN
1966
1967 Check/untaint signed numeric data (whole numbers).  If there is an error,
1968 returns the error, otherwise returns false.
1969
1970 =cut
1971
1972 sub ut_snumber {
1973   my($self, $field) = @_;
1974   $self->getfield($field) =~ /^\s*(-?)\s*(\d+)\s*$/
1975     or return "Illegal or empty (numeric) $field: ". $self->getfield($field);
1976   $self->setfield($field, "$1$2");
1977   '';
1978 }
1979
1980 =item ut_snumbern COLUMN
1981
1982 Check/untaint signed numeric data (whole numbers).  If there is an error,
1983 returns the error, otherwise returns false.
1984
1985 =cut
1986
1987 sub ut_snumbern {
1988   my($self, $field) = @_;
1989   $self->getfield($field) =~ /^\s*(-?)\s*(\d*)\s*$/
1990     or return "Illegal (numeric) $field: ". $self->getfield($field);
1991   if ($1) {
1992     return "Illegal (numeric) $field: ". $self->getfield($field)
1993       unless $2;
1994   }
1995   $self->setfield($field, "$1$2");
1996   '';
1997 }
1998
1999 =item ut_number COLUMN
2000
2001 Check/untaint simple numeric data (whole numbers).  May not be null.  If there
2002 is an error, returns the error, otherwise returns false.
2003
2004 =cut
2005
2006 sub ut_number {
2007   my($self,$field)=@_;
2008   $self->getfield($field) =~ /^\s*(\d+)\s*$/
2009     or return "Illegal or empty (numeric) $field: ". $self->getfield($field);
2010   $self->setfield($field,$1);
2011   '';
2012 }
2013
2014 =item ut_numbern COLUMN
2015
2016 Check/untaint simple numeric data (whole numbers).  May be null.  If there is
2017 an error, returns the error, otherwise returns false.
2018
2019 =cut
2020
2021 sub ut_numbern {
2022   my($self,$field)=@_;
2023   $self->getfield($field) =~ /^\s*(\d*)\s*$/
2024     or return "Illegal (numeric) $field: ". $self->getfield($field);
2025   $self->setfield($field,$1);
2026   '';
2027 }
2028
2029 =item ut_money COLUMN
2030
2031 Check/untaint monetary numbers.  May be negative.  Set to 0 if null.  If there
2032 is an error, returns the error, otherwise returns false.
2033
2034 =cut
2035
2036 sub ut_money {
2037   my($self,$field)=@_;
2038   $self->setfield($field, 0) if $self->getfield($field) eq '';
2039   $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/
2040     or return "Illegal (money) $field: ". $self->getfield($field);
2041   #$self->setfield($field, "$1$2$3" || 0);
2042   $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0);
2043   '';
2044 }
2045
2046 =item ut_moneyn COLUMN
2047
2048 Check/untaint monetary numbers.  May be negative.  If there
2049 is an error, returns the error, otherwise returns false.
2050
2051 =cut
2052
2053 sub ut_moneyn {
2054   my($self,$field)=@_;
2055   if ($self->getfield($field) eq '') {
2056     $self->setfield($field, '');
2057     return '';
2058   }
2059   $self->ut_money($field);
2060 }
2061
2062 =item ut_text COLUMN
2063
2064 Check/untaint text.  Alphanumerics, spaces, and the following punctuation
2065 symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < >
2066 May not be null.  If there is an error, returns the error, otherwise returns
2067 false.
2068
2069 =cut
2070
2071 sub ut_text {
2072   my($self,$field)=@_;
2073   #warn "msgcat ". \&msgcat. "\n";
2074   #warn "notexist ". \&notexist. "\n";
2075   #warn "AUTOLOAD ". \&AUTOLOAD. "\n";
2076   $self->getfield($field)
2077     =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>]+)$/
2078       or return gettext('illegal_or_empty_text'). " $field: ".
2079                  $self->getfield($field);
2080   $self->setfield($field,$1);
2081   '';
2082 }
2083
2084 =item ut_textn COLUMN
2085
2086 Check/untaint text.  Alphanumerics, spaces, and the following punctuation
2087 symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? /
2088 May be null.  If there is an error, returns the error, otherwise returns false.
2089
2090 =cut
2091
2092 sub ut_textn {
2093   my($self,$field)=@_;
2094   $self->getfield($field)
2095     =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]]*)$/
2096       or return gettext('illegal_text'). " $field: ". $self->getfield($field);
2097   $self->setfield($field,$1);
2098   '';
2099 }
2100
2101 =item ut_alpha COLUMN
2102
2103 Check/untaint alphanumeric strings (no spaces).  May not be null.  If there is
2104 an error, returns the error, otherwise returns false.
2105
2106 =cut
2107
2108 sub ut_alpha {
2109   my($self,$field)=@_;
2110   $self->getfield($field) =~ /^(\w+)$/
2111     or return "Illegal or empty (alphanumeric) $field: ".
2112               $self->getfield($field);
2113   $self->setfield($field,$1);
2114   '';
2115 }
2116
2117 =item ut_alpha COLUMN
2118
2119 Check/untaint alphanumeric strings (no spaces).  May be null.  If there is an
2120 error, returns the error, otherwise returns false.
2121
2122 =cut
2123
2124 sub ut_alphan {
2125   my($self,$field)=@_;
2126   $self->getfield($field) =~ /^(\w*)$/ 
2127     or return "Illegal (alphanumeric) $field: ". $self->getfield($field);
2128   $self->setfield($field,$1);
2129   '';
2130 }
2131
2132 =item ut_alpha_lower COLUMN
2133
2134 Check/untaint lowercase alphanumeric strings (no spaces).  May not be null.  If
2135 there is an error, returns the error, otherwise returns false.
2136
2137 =cut
2138
2139 sub ut_alpha_lower {
2140   my($self,$field)=@_;
2141   $self->getfield($field) =~ /[[:upper:]]/
2142     and return "Uppercase characters are not permitted in $field";
2143   $self->ut_alpha($field);
2144 }
2145
2146 =item ut_phonen COLUMN [ COUNTRY ]
2147
2148 Check/untaint phone numbers.  May be null.  If there is an error, returns
2149 the error, otherwise returns false.
2150
2151 Takes an optional two-letter ISO country code; without it or with unsupported
2152 countries, ut_phonen simply calls ut_alphan.
2153
2154 =cut
2155
2156 sub ut_phonen {
2157   my( $self, $field, $country ) = @_;
2158   return $self->ut_alphan($field) unless defined $country;
2159   my $phonen = $self->getfield($field);
2160   if ( $phonen eq '' ) {
2161     $self->setfield($field,'');
2162   } elsif ( $country eq 'US' || $country eq 'CA' ) {
2163     $phonen =~ s/\D//g;
2164     $phonen = $conf->config('cust_main-default_areacode').$phonen
2165       if length($phonen)==7 && $conf->config('cust_main-default_areacode');
2166     $phonen =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/
2167       or return gettext('illegal_phone'). " $field: ". $self->getfield($field);
2168     $phonen = "$1-$2-$3";
2169     $phonen .= " x$4" if $4;
2170     $self->setfield($field,$phonen);
2171   } else {
2172     warn "warning: don't know how to check phone numbers for country $country";
2173     return $self->ut_textn($field);
2174   }
2175   '';
2176 }
2177
2178 =item ut_hex COLUMN
2179
2180 Check/untaint hexadecimal values.
2181
2182 =cut
2183
2184 sub ut_hex {
2185   my($self, $field) = @_;
2186   $self->getfield($field) =~ /^([\da-fA-F]+)$/
2187     or return "Illegal (hex) $field: ". $self->getfield($field);
2188   $self->setfield($field, uc($1));
2189   '';
2190 }
2191
2192 =item ut_hexn COLUMN
2193
2194 Check/untaint hexadecimal values.  May be null.
2195
2196 =cut
2197
2198 sub ut_hexn {
2199   my($self, $field) = @_;
2200   $self->getfield($field) =~ /^([\da-fA-F]*)$/
2201     or return "Illegal (hex) $field: ". $self->getfield($field);
2202   $self->setfield($field, uc($1));
2203   '';
2204 }
2205 =item ut_ip COLUMN
2206
2207 Check/untaint ip addresses.  IPv4 only for now.
2208
2209 =cut
2210
2211 sub ut_ip {
2212   my( $self, $field ) = @_;
2213   $self->getfield($field) =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
2214     or return "Illegal (IP address) $field: ". $self->getfield($field);
2215   for ( $1, $2, $3, $4 ) { return "Illegal (IP address) $field" if $_ > 255; }
2216   $self->setfield($field, "$1.$2.$3.$4");
2217   '';
2218 }
2219
2220 =item ut_ipn COLUMN
2221
2222 Check/untaint ip addresses.  IPv4 only for now.  May be null.
2223
2224 =cut
2225
2226 sub ut_ipn {
2227   my( $self, $field ) = @_;
2228   if ( $self->getfield($field) =~ /^()$/ ) {
2229     $self->setfield($field,'');
2230     '';
2231   } else {
2232     $self->ut_ip($field);
2233   }
2234 }
2235
2236 =item ut_coord COLUMN [ LOWER [ UPPER ] ]
2237
2238 Check/untaint coordinates.
2239 Accepts the following forms:
2240 DDD.DDDDD
2241 -DDD.DDDDD
2242 DDD MM.MMM
2243 -DDD MM.MMM
2244 DDD MM SS
2245 -DDD MM SS
2246 DDD MM MMM
2247 -DDD MM MMM
2248
2249 The "DDD MM SS" and "DDD MM MMM" are potentially ambiguous.
2250 The latter form (that is, the MMM are thousands of minutes) is
2251 assumed if the "MMM" is exactly three digits or two digits > 59.
2252
2253 To be safe, just use the DDD.DDDDD form.
2254
2255 If LOWER or UPPER are specified, then the coordinate is checked
2256 for lower and upper bounds, respectively.
2257
2258 =cut
2259
2260 sub ut_coord {
2261
2262   my ($self, $field) = (shift, shift);
2263
2264   my $lower = shift if scalar(@_);
2265   my $upper = shift if scalar(@_);
2266   my $coord = $self->getfield($field);
2267   my $neg = $coord =~ s/^(-)//;
2268
2269   my ($d, $m, $s) = (0, 0, 0);
2270
2271   if (
2272     (($d) = ($coord =~ /^(\s*\d{1,3}(?:\.\d+)?)\s*$/)) ||
2273     (($d, $m) = ($coord =~ /^(\s*\d{1,3})\s+(\d{1,2}(?:\.\d+))\s*$/)) ||
2274     (($d, $m, $s) = ($coord =~ /^(\s*\d{1,3})\s+(\d{1,2})\s+(\d{1,3})\s*$/))
2275   ) {
2276     $s = (((($s =~ /^\d{3}$/) or $s > 59) ? ($s / 1000) : ($s / 60)) / 60);
2277     $m = $m / 60;
2278     if ($m > 59) {
2279       return "Invalid (coordinate with minutes > 59) $field: "
2280              . $self->getfield($field);
2281     }
2282
2283     $coord = ($neg ? -1 : 1) * sprintf('%.8f', $d + $m + $s);
2284
2285     if (defined($lower) and ($coord < $lower)) {
2286       return "Invalid (coordinate < $lower) $field: "
2287              . $self->getfield($field);;
2288     }
2289
2290     if (defined($upper) and ($coord > $upper)) {
2291       return "Invalid (coordinate > $upper) $field: "
2292              . $self->getfield($field);;
2293     }
2294
2295     $self->setfield($field, $coord);
2296     return '';
2297   }
2298
2299   return "Invalid (coordinate) $field: " . $self->getfield($field);
2300
2301 }
2302
2303 =item ut_coordn COLUMN [ LOWER [ UPPER ] ]
2304
2305 Same as ut_coord, except optionally null.
2306
2307 =cut
2308
2309 sub ut_coordn {
2310
2311   my ($self, $field) = (shift, shift);
2312
2313   if ($self->getfield($field) =~ /^$/) {
2314     return '';
2315   } else {
2316     return $self->ut_coord($field, @_);
2317   }
2318
2319 }
2320
2321
2322 =item ut_domain COLUMN
2323
2324 Check/untaint host and domain names.
2325
2326 =cut
2327
2328 sub ut_domain {
2329   my( $self, $field ) = @_;
2330   #$self->getfield($field) =~/^(\w+\.)*\w+$/
2331   $self->getfield($field) =~/^(([\w\-]+\.)*\w+)$/
2332     or return "Illegal (domain) $field: ". $self->getfield($field);
2333   $self->setfield($field,$1);
2334   '';
2335 }
2336
2337 =item ut_name COLUMN
2338
2339 Check/untaint proper names; allows alphanumerics, spaces and the following
2340 punctuation: , . - '
2341
2342 May not be null.
2343
2344 =cut
2345
2346 sub ut_name {
2347   my( $self, $field ) = @_;
2348   $self->getfield($field) =~ /^([\w \,\.\-\']+)$/
2349     or return gettext('illegal_name'). " $field: ". $self->getfield($field);
2350   $self->setfield($field,$1);
2351   '';
2352 }
2353
2354 =item ut_zip COLUMN
2355
2356 Check/untaint zip codes.
2357
2358 =cut
2359
2360 my @zip_reqd_countries = qw( AU CA US ); #CA, US implicit...
2361
2362 sub ut_zip {
2363   my( $self, $field, $country ) = @_;
2364
2365   if ( $country eq 'US' ) {
2366
2367     $self->getfield($field) =~ /^\s*(\d{5}(\-\d{4})?)\s*$/
2368       or return gettext('illegal_zip'). " $field for country $country: ".
2369                 $self->getfield($field);
2370     $self->setfield($field, $1);
2371
2372   } elsif ( $country eq 'CA' ) {
2373
2374     $self->getfield($field) =~ /^\s*([A-Z]\d[A-Z])\s*(\d[A-Z]\d)\s*$/i
2375       or return gettext('illegal_zip'). " $field for country $country: ".
2376                 $self->getfield($field);
2377     $self->setfield($field, "$1 $2");
2378
2379   } else {
2380
2381     if ( $self->getfield($field) =~ /^\s*$/
2382          && ( !$country || ! grep { $_ eq $country } @zip_reqd_countries )
2383        )
2384     {
2385       $self->setfield($field,'');
2386     } else {
2387       $self->getfield($field) =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
2388         or return gettext('illegal_zip'). " $field: ". $self->getfield($field);
2389       $self->setfield($field,$1);
2390     }
2391
2392   }
2393
2394   '';
2395 }
2396
2397 =item ut_country COLUMN
2398
2399 Check/untaint country codes.  Country names are changed to codes, if possible -
2400 see L<Locale::Country>.
2401
2402 =cut
2403
2404 sub ut_country {
2405   my( $self, $field ) = @_;
2406   unless ( $self->getfield($field) =~ /^(\w\w)$/ ) {
2407     if ( $self->getfield($field) =~ /^([\w \,\.\(\)\']+)$/ 
2408          && country2code($1) ) {
2409       $self->setfield($field,uc(country2code($1)));
2410     }
2411   }
2412   $self->getfield($field) =~ /^(\w\w)$/
2413     or return "Illegal (country) $field: ". $self->getfield($field);
2414   $self->setfield($field,uc($1));
2415   '';
2416 }
2417
2418 =item ut_anything COLUMN
2419
2420 Untaints arbitrary data.  Be careful.
2421
2422 =cut
2423
2424 sub ut_anything {
2425   my( $self, $field ) = @_;
2426   $self->getfield($field) =~ /^(.*)$/s
2427     or return "Illegal $field: ". $self->getfield($field);
2428   $self->setfield($field,$1);
2429   '';
2430 }
2431
2432 =item ut_enum COLUMN CHOICES_ARRAYREF
2433
2434 Check/untaint a column, supplying all possible choices, like the "enum" type.
2435
2436 =cut
2437
2438 sub ut_enum {
2439   my( $self, $field, $choices ) = @_;
2440   foreach my $choice ( @$choices ) {
2441     if ( $self->getfield($field) eq $choice ) {
2442       $self->setfield($field, $choice);
2443       return '';
2444     }
2445   }
2446   return "Illegal (enum) field $field: ". $self->getfield($field);
2447 }
2448
2449 =item ut_foreign_key COLUMN FOREIGN_TABLE FOREIGN_COLUMN
2450
2451 Check/untaint a foreign column key.  Call a regular ut_ method (like ut_number)
2452 on the column first.
2453
2454 =cut
2455
2456 sub ut_foreign_key {
2457   my( $self, $field, $table, $foreign ) = @_;
2458   return '' if $no_check_foreign;
2459   qsearchs($table, { $foreign => $self->getfield($field) })
2460     or return "Can't find ". $self->table. ".$field ". $self->getfield($field).
2461               " in $table.$foreign";
2462   '';
2463 }
2464
2465 =item ut_foreign_keyn COLUMN FOREIGN_TABLE FOREIGN_COLUMN
2466
2467 Like ut_foreign_key, except the null value is also allowed.
2468
2469 =cut
2470
2471 sub ut_foreign_keyn {
2472   my( $self, $field, $table, $foreign ) = @_;
2473   $self->getfield($field)
2474     ? $self->ut_foreign_key($field, $table, $foreign)
2475     : '';
2476 }
2477
2478 =item ut_agentnum_acl COLUMN [ NULL_RIGHT | NULL_RIGHT_LISTREF ]
2479
2480 Checks this column as an agentnum, taking into account the current users's
2481 ACLs.  NULL_RIGHT or NULL_RIGHT_LISTREF, if specified, indicates the access
2482 right or rights allowing no agentnum.
2483
2484 =cut
2485
2486 sub ut_agentnum_acl {
2487   my( $self, $field ) = (shift, shift);
2488   my $null_acl = scalar(@_) ? shift : [];
2489   $null_acl = [ $null_acl ] unless ref($null_acl);
2490
2491   my $error = $self->ut_foreign_keyn($field, 'agent', 'agentnum');
2492   return "Illegal agentnum: $error" if $error;
2493
2494   my $curuser = $FS::CurrentUser::CurrentUser;
2495
2496   if ( $self->$field() ) {
2497
2498     return "Access denied"
2499       unless $curuser->agentnum($self->$field());
2500
2501   } else {
2502
2503     return "Access denied"
2504       unless grep $curuser->access_right($_), @$null_acl;
2505
2506   }
2507
2508   '';
2509
2510 }
2511
2512 =item virtual_fields [ TABLE ]
2513
2514 Returns a list of virtual fields defined for the table.  This should not 
2515 be exported, and should only be called as an instance or class method.
2516
2517 =cut
2518
2519 sub virtual_fields {
2520   my $self = shift;
2521   my $table;
2522   $table = $self->table or confess "virtual_fields called on non-table";
2523
2524   confess "Unknown table $table" unless dbdef->table($table);
2525
2526   return () unless dbdef->table('part_virtual_field');
2527
2528   unless ( $virtual_fields_cache{$table} ) {
2529     my $query = 'SELECT name from part_virtual_field ' .
2530                 "WHERE dbtable = '$table'";
2531     my $dbh = dbh;
2532     my $result = $dbh->selectcol_arrayref($query);
2533     confess "Error executing virtual fields query: $query: ". $dbh->errstr
2534       if $dbh->err;
2535     $virtual_fields_cache{$table} = $result;
2536   }
2537
2538   @{$virtual_fields_cache{$table}};
2539
2540 }
2541
2542
2543 =item fields [ TABLE ]
2544
2545 This is a wrapper for real_fields and virtual_fields.  Code that called
2546 fields before should probably continue to call fields.
2547
2548 =cut
2549
2550 sub fields {
2551   my $something = shift;
2552   my $table;
2553   if($something->isa('FS::Record')) {
2554     $table = $something->table;
2555   } else {
2556     $table = $something;
2557     $something = "FS::$table";
2558   }
2559   return (real_fields($table), $something->virtual_fields());
2560 }
2561
2562 =item pvf FIELD_NAME
2563
2564 Returns the FS::part_virtual_field object corresponding to a field in the 
2565 record (specified by FIELD_NAME).
2566
2567 =cut
2568
2569 sub pvf {
2570   my ($self, $name) = (shift, shift);
2571
2572   if(grep /^$name$/, $self->virtual_fields) {
2573     return qsearchs('part_virtual_field', { dbtable => $self->table,
2574                                             name    => $name } );
2575   }
2576   ''
2577 }
2578
2579 =item vfieldpart_hashref TABLE
2580
2581 Returns a hashref of virtual field names and vfieldparts applicable to the given
2582 TABLE.
2583
2584 =cut
2585
2586 sub vfieldpart_hashref {
2587   my $self = shift;
2588   my $table = $self->table;
2589
2590   return {} unless dbdef->table('part_virtual_field');
2591
2592   my $dbh = dbh;
2593   my $statement = "SELECT vfieldpart, name FROM part_virtual_field WHERE ".
2594                   "dbtable = '$table'";
2595   my $sth = $dbh->prepare($statement);
2596   $sth->execute or croak "Execution of '$statement' failed: ".$dbh->errstr;
2597   return { map { $_->{name}, $_->{vfieldpart} } 
2598     @{$sth->fetchall_arrayref({})} };
2599
2600 }
2601
2602 =item encrypt($value)
2603
2604 Encrypts the credit card using a combination of PK to encrypt and uuencode to armour.
2605
2606 Returns the encrypted string.
2607
2608 You should generally not have to worry about calling this, as the system handles this for you.
2609
2610 =cut
2611
2612 sub encrypt {
2613   my ($self, $value) = @_;
2614   my $encrypted;
2615
2616   if ($conf->exists('encryption')) {
2617     if ($self->is_encrypted($value)) {
2618       # Return the original value if it isn't plaintext.
2619       $encrypted = $value;
2620     } else {
2621       $self->loadRSA;
2622       if (ref($rsa_encrypt) =~ /::RSA/) { # We Can Encrypt
2623         # RSA doesn't like the empty string so let's pack it up
2624         # The database doesn't like the RSA data so uuencode it
2625         my $length = length($value)+1;
2626         $encrypted = pack("u*",$rsa_encrypt->encrypt(pack("Z$length",$value)));
2627       } else {
2628         die ("You can't encrypt w/o a valid RSA engine - Check your installation or disable encryption");
2629       }
2630     }
2631   }
2632   return $encrypted;
2633 }
2634
2635 =item is_encrypted($value)
2636
2637 Checks to see if the string is encrypted and returns true or false (1/0) to indicate it's status.
2638
2639 =cut
2640
2641
2642 sub is_encrypted {
2643   my ($self, $value) = @_;
2644   # Possible Bug - Some work may be required here....
2645
2646   if ($value =~ /^M/ && length($value) > 80) {
2647     return 1;
2648   } else {
2649     return 0;
2650   }
2651 }
2652
2653 =item decrypt($value)
2654
2655 Uses the private key to decrypt the string. Returns the decryoted string or undef on failure.
2656
2657 You should generally not have to worry about calling this, as the system handles this for you.
2658
2659 =cut
2660
2661 sub decrypt {
2662   my ($self,$value) = @_;
2663   my $decrypted = $value; # Will return the original value if it isn't encrypted or can't be decrypted.
2664   if ($conf->exists('encryption') && $self->is_encrypted($value)) {
2665     $self->loadRSA;
2666     if (ref($rsa_decrypt) =~ /::RSA/) {
2667       my $encrypted = unpack ("u*", $value);
2668       $decrypted =  unpack("Z*", eval{$rsa_decrypt->decrypt($encrypted)});
2669       if ($@) {warn "Decryption Failed"};
2670     }
2671   }
2672   return $decrypted;
2673 }
2674
2675 sub loadRSA {
2676     my $self = shift;
2677     #Initialize the Module
2678     $rsa_module = 'Crypt::OpenSSL::RSA'; # The Default
2679
2680     if ($conf->exists('encryptionmodule') && $conf->config_binary('encryptionmodule') ne '') {
2681       $rsa_module = $conf->config('encryptionmodule');
2682     }
2683
2684     if (!$rsa_loaded) {
2685         eval ("require $rsa_module"); # No need to import the namespace
2686         $rsa_loaded++;
2687     }
2688     # Initialize Encryption
2689     if ($conf->exists('encryptionpublickey') && $conf->config_binary('encryptionpublickey') ne '') {
2690       my $public_key = join("\n",$conf->config('encryptionpublickey'));
2691       $rsa_encrypt = $rsa_module->new_public_key($public_key);
2692     }
2693     
2694     # Intitalize Decryption
2695     if ($conf->exists('encryptionprivatekey') && $conf->config_binary('encryptionprivatekey') ne '') {
2696       my $private_key = join("\n",$conf->config('encryptionprivatekey'));
2697       $rsa_decrypt = $rsa_module->new_private_key($private_key);
2698     }
2699 }
2700
2701 =item h_search ACTION
2702
2703 Given an ACTION, either "insert", or "delete", returns the appropriate history
2704 record corresponding to this record, if any.
2705
2706 =cut
2707
2708 sub h_search {
2709   my( $self, $action ) = @_;
2710
2711   my $table = $self->table;
2712   $table =~ s/^h_//;
2713
2714   my $primary_key = dbdef->table($table)->primary_key;
2715
2716   qsearchs({
2717     'table'   => "h_$table",
2718     'hashref' => { $primary_key     => $self->$primary_key(),
2719                    'history_action' => $action,
2720                  },
2721   });
2722
2723 }
2724
2725 =item h_date ACTION
2726
2727 Given an ACTION, either "insert", or "delete", returns the timestamp of the
2728 appropriate history record corresponding to this record, if any.
2729
2730 =cut
2731
2732 sub h_date {
2733   my($self, $action) = @_;
2734   my $h = $self->h_search($action);
2735   $h ? $h->history_date : '';
2736 }
2737
2738 =back
2739
2740 =head1 SUBROUTINES
2741
2742 =over 4
2743
2744 =item real_fields [ TABLE ]
2745
2746 Returns a list of the real columns in the specified table.  Called only by 
2747 fields() and other subroutines elsewhere in FS::Record.
2748
2749 =cut
2750
2751 sub real_fields {
2752   my $table = shift;
2753
2754   my($table_obj) = dbdef->table($table);
2755   confess "Unknown table $table" unless $table_obj;
2756   $table_obj->columns;
2757 }
2758
2759 =item _quote VALUE, TABLE, COLUMN
2760
2761 This is an internal function used to construct SQL statements.  It returns
2762 VALUE DBI-quoted (see L<DBI/"quote">) unless VALUE is a number and the column
2763 type (see L<DBIx::DBSchema::Column>) does not end in `char' or `binary'.
2764
2765 =cut
2766
2767 sub _quote {
2768   my($value, $table, $column) = @_;
2769   my $column_obj = dbdef->table($table)->column($column);
2770   my $column_type = $column_obj->type;
2771   my $nullable = $column_obj->null;
2772
2773   warn "  $table.$column: $value ($column_type".
2774        ( $nullable ? ' NULL' : ' NOT NULL' ).
2775        ")\n" if $DEBUG > 2;
2776
2777   if ( $value eq '' && $nullable ) {
2778     'NULL';
2779   } elsif ( $value eq '' && $column_type =~ /^(int|numeric)/ ) {
2780     cluck "WARNING: Attempting to set non-null integer $table.$column null; ".
2781           "using 0 instead";
2782     0;
2783   } elsif ( $value =~ /^\d+(\.\d+)?$/ && 
2784             ! $column_type =~ /(char|binary|text)$/i ) {
2785     $value;
2786   } elsif (( $column_type =~ /^bytea$/i || $column_type =~ /(blob|varbinary)/i )
2787            && driver_name eq 'Pg'
2788           )
2789   {
2790     no strict 'subs';
2791 #    dbh->quote($value, { pg_type => PG_BYTEA() }); # doesn't work right
2792     # Pg binary string quoting: convert each character to 3-digit octal prefixed with \\, 
2793     # single-quote the whole mess, and put an "E" in front.
2794     return ("E'" . join('', map { sprintf('\\\\%03o', ord($_)) } split(//, $value) ) . "'");
2795   } else {
2796     dbh->quote($value);
2797   }
2798 }
2799
2800 =item hfields TABLE
2801
2802 This is deprecated.  Don't use it.
2803
2804 It returns a hash-type list with the fields of this record's table set true.
2805
2806 =cut
2807
2808 sub hfields {
2809   carp "warning: hfields is deprecated";
2810   my($table)=@_;
2811   my(%hash);
2812   foreach (fields($table)) {
2813     $hash{$_}=1;
2814   }
2815   \%hash;
2816 }
2817
2818 sub _dump {
2819   my($self)=@_;
2820   join("\n", map {
2821     "$_: ". $self->getfield($_). "|"
2822   } (fields($self->table)) );
2823 }
2824
2825 sub DESTROY { return; }
2826
2827 #sub DESTROY {
2828 #  my $self = shift;
2829 #  #use Carp qw(cluck);
2830 #  #cluck "DESTROYING $self";
2831 #  warn "DESTROYING $self";
2832 #}
2833
2834 #sub is_tainted {
2835 #             return ! eval { join('',@_), kill 0; 1; };
2836 #         }
2837
2838 =item str2time_sql [ DRIVER_NAME ]
2839
2840 Returns a function to convert to unix time based on database type, such as
2841 "EXTRACT( EPOCH FROM" for Pg or "UNIX_TIMESTAMP(" for mysql.  See
2842 the str2time_sql_closing method to return a closing string rather than just
2843 using a closing parenthesis as previously suggested.
2844
2845 You can pass an optional driver name such as "Pg", "mysql" or
2846 $dbh->{Driver}->{Name} to return a function for that database instead of
2847 the current database.
2848
2849 =cut
2850
2851 sub str2time_sql { 
2852   my $driver = shift || driver_name;
2853
2854   return 'UNIX_TIMESTAMP('      if $driver =~ /^mysql/i;
2855   return 'EXTRACT( EPOCH FROM ' if $driver =~ /^Pg/i;
2856
2857   warn "warning: unknown database type $driver; guessing how to convert ".
2858        "dates to UNIX timestamps";
2859   return 'EXTRACT(EPOCH FROM ';
2860
2861 }
2862
2863 =item str2time_sql_closing [ DRIVER_NAME ]
2864
2865 Returns the closing suffix of a function to convert to unix time based on
2866 database type, such as ")::integer" for Pg or ")" for mysql.
2867
2868 You can pass an optional driver name such as "Pg", "mysql" or
2869 $dbh->{Driver}->{Name} to return a function for that database instead of
2870 the current database.
2871
2872 =cut
2873
2874 sub str2time_sql_closing { 
2875   my $driver = shift || driver_name;
2876
2877   return ' )::INTEGER ' if $driver =~ /^Pg/i;
2878   return ' ) ';
2879 }
2880
2881 =back
2882
2883 =head1 BUGS
2884
2885 This module should probably be renamed, since much of the functionality is
2886 of general use.  It is not completely unlike Adapter::DBI (see below).
2887
2888 Exported qsearch and qsearchs should be deprecated in favor of method calls
2889 (against an FS::Record object like the old search and searchs that qsearch
2890 and qsearchs were on top of.)
2891
2892 The whole fields / hfields mess should be removed.
2893
2894 The various WHERE clauses should be subroutined.
2895
2896 table string should be deprecated in favor of DBIx::DBSchema::Table.
2897
2898 No doubt we could benefit from a Tied hash.  Documenting how exists / defined
2899 true maps to the database (and WHERE clauses) would also help.
2900
2901 The ut_ methods should ask the dbdef for a default length.
2902
2903 ut_sqltype (like ut_varchar) should all be defined
2904
2905 A fallback check method should be provided which uses the dbdef.
2906
2907 The ut_money method assumes money has two decimal digits.
2908
2909 The Pg money kludge in the new method only strips `$'.
2910
2911 The ut_phonen method only checks US-style phone numbers.
2912
2913 The _quote function should probably use ut_float instead of a regex.
2914
2915 All the subroutines probably should be methods, here or elsewhere.
2916
2917 Probably should borrow/use some dbdef methods where appropriate (like sub
2918 fields)
2919
2920 As of 1.14, DBI fetchall_hashref( {} ) doesn't set fetchrow_hashref NAME_lc,
2921 or allow it to be set.  Working around it is ugly any way around - DBI should
2922 be fixed.  (only affects RDBMS which return uppercase column names)
2923
2924 ut_zip should take an optional country like ut_phone.
2925
2926 =head1 SEE ALSO
2927
2928 L<DBIx::DBSchema>, L<FS::UID>, L<DBI>
2929
2930 Adapter::DBI from Ch. 11 of Advanced Perl Programming by Sriram Srinivasan.
2931
2932 http://poop.sf.net/
2933
2934 =cut
2935
2936 1;
2937