1 # Vend::Scan - Prepare searches for Interchange
3 # $Id: Scan.pm,v 2.35 2008-07-07 18:15:07 docelic Exp $
5 # Copyright (C) 2002-2007 Interchange Development Group
6 # Copyright (C) 1996-2002 Red Hat, Inc.
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public
19 # License along with this program; if not, write to the Free
20 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
33 $VERSION = substr(q$Revision: 2.35 $, 10);
36 no warnings qw(uninitialized numeric);
40 use Vend::Interpolate;
41 use Vend::Data qw(product_code_exists_ref column_index);
119 ## Place marker, not used in search specs but is reserved
121 ## hf mv_header_fields
130 cv mv_verbatim_columns
139 er mv_spelling_errors
140 fc mv_force_coordinate
151 lr mv_search_line_return
154 mc mv_more_alpha_chars
175 re mv_search_reference
180 rn mv_return_file_name
181 rr mv_return_reference
188 si mv_search_immediate
194 su mv_substring_match
201 my @ScanKeys = keys %Scan;
203 %RevScan = reverse %Scan;
206 mv_search_group => \&_array,
207 mv_search_field => \&_array,
208 mv_all_chars => \&_yes_array,
209 mv_begin_string => \&_yes_array,
210 mv_case => \&_yes_array,
211 mv_negate => \&_yes_array,
212 mv_numeric => \&_yes_array,
213 mv_orsearch => \&_yes_array,
214 mv_substring_match => \&_yes_array,
215 mv_column_op => \&_array,
216 mv_coordinate => \&_yes,
217 mv_no_hide => \&_yes,
218 mv_no_more => \&_yes,
219 mv_field_names => \&_array,
220 mv_spelling_errors => sub { my $n = int($_[1]); $n < 8 ? $n : 1; },
221 mv_dict_limit => \&_dict_limit,
222 mv_exact_match => \&_yes,
223 mv_head_skip => \&_number,
224 mv_matchlimit => \&_matchlimit,
225 mv_max_matches => sub { $_[1] =~ /(\d+)/ ? $1 : -1 },
226 mv_min_string => sub { $_[1] =~ /(\d+)/ ? $1 : 1 },
227 mv_profile => \&parse_profile,
228 mv_range_alpha => \&_array,
229 mv_range_look => \&_array,
230 mv_range_max => \&_array,
231 mv_range_min => \&_array,
232 mv_return_all => \&_yes,
233 mv_return_fields => \&_array,
234 mv_return_file_name => \&_yes,
235 mv_save_context => \&_array,
236 mv_searchspec => \&_verbatim_array,
237 mv_like_field => \&_array,
238 mv_like_spec => \&_verbatim_array,
239 mv_sort_field => \&_array,
240 mv_sort_option => \&_opt,
242 mv_value => \&_value,
243 mv_sql_query => sub {
245 my $p = Vend::Interpolate::escape_scan($val, $ref);
246 find_search_params($ref, $p);
249 base_directory => \&_dir_security_scalar,
250 mv_field_file => \&_file_security_scalar,
251 mv_search_file => \&_file_security,
252 mv_more_alpha => \&_yes,
253 mv_more_alpha_chars => sub { $_[1] =~ /(\d+)/ ? $1 : 3 },
256 sub create_last_search {
261 while( ($key, $val) = each %$ref) {
262 next unless defined $RevScan{$key};
263 @val = split /\0/, $val;
266 s!(\W)!sprintf '%%%02x', ord($1)!eg;
268 push @out, "$RevScan{$key}=$_";
272 # Make repeatable for permanent store
275 $Vend::Session->{last_search} = join "/", 'scan', @out;
278 sub find_search_params {
282 $param =~ s/-_NULL_-/\0/g;
283 @args = split m:/:, $param;
289 ($var,$val) = split /=/, $_, 2;
290 next unless defined $Scan{$var};
292 $c->{$Scan{$var}} = defined $c->{$Scan{$var}}
293 ? ($c->{$Scan{$var}} . "\0$val" )
296 #::logDebug("find_search_params: " . ::uneval($c));
304 $map = delete $ref->{mv_search_map} unless $map;
306 return undef unless defined $map;
308 if(index($map, "\n") != -1) {
311 elsif(defined $Vend::Cfg->{SearchProfileName}->{$map}) {
312 $map = $Vend::Cfg->{SearchProfileName}->{$map};
313 $params = $Vend::Cfg->{SearchProfile}->[$map];
315 elsif($map =~ /^\d+$/) {
316 $params = $Vend::Cfg->{SearchProfile}->[$map];
318 elsif(defined $::Scratch->{$map}) {
319 $params = $::Scratch->{$map};
322 return undef unless $params;
324 if ( $params =~ m{\[} or $params =~ /__/) {
325 $params = interpolate_html($params);
328 my($ary, $var,$source, $i);
330 $params =~ s/^\s+//mg;
331 $params =~ s/\s+$//mg;
332 my(@param) = grep $_, split /[\r\n]+/, $params;
334 ($var,$source) = split /[\s=]+/, $_, 2;
335 $ref->{$var} = [] unless defined $ref->{$var};
336 $ref->{$source} = '' if ! defined $ref->{$source};
337 $ref->{$source} =~ s/\0/|/g;
338 push @{$ref->{$var}}, ($ref->{$source});
343 sub parse_profile_ref {
344 my ($ref, $profile) = @_;
346 foreach $p (keys %$profile) {
350 (defined $RevScan{$p} and $var = $p);
351 $ref->{$var} = $profile->{$p}, next
352 if ref $profile->{$p} || ! defined $Parse{$var};
353 $ref->{$var} = &{$Parse{$var}}($ref,$profile->{$p});
359 my($ref,$profile) = @_;
360 return undef unless defined $profile;
362 if(defined $Vend::Cfg->{SearchProfileName}->{$profile}) {
363 $profile = $Vend::Cfg->{SearchProfileName}->{$profile};
364 $params = $Vend::Cfg->{SearchProfile}->[$profile];
366 elsif($profile =~ /^\d+$/) {
367 $params = $Vend::Cfg->{SearchProfile}->[$profile];
369 elsif(defined $::Scratch->{$profile}) {
370 $params = $::Scratch->{$profile};
373 return undef unless $params;
375 if ( index($params, '[') != -1 or index($params, '__') != -1) {
376 $params = ::interpolate_html($params);
380 my $status = $profile;
382 $params =~ s/^\s+//mg;
383 $params =~ s/\s+$//mg;
384 my(@param) = grep $_, split /[\r\n]+/, $params;
386 ($var,$val) = split /[\s=]+/, $_, 2;
387 $status = -1 if $var eq 'mv_last';
388 next unless defined $RevScan{$var} or $var = $Scan{$var};
389 $val =~ s/&#(\d+);/chr($1)/ge;
391 $val = &{$Parse{$var}}($ref,$val,$ref->{$var} || undef)
392 if defined $Parse{$var};
393 $ref->{$var} = $val if defined $val;
401 #::logDebug("finishing up search spec=" . ::uneval($q));
402 my $matches = $q->{'matches'};
403 $::Values->{mv_search_match_count} = $matches;
404 delete $::Values->{mv_search_error};
405 $::Values->{mv_search_error} = $q->{mv_search_error}
406 if $q->{mv_search_error};
407 $::Values->{mv_matchlimit} = $q->{mv_matchlimit};
408 $::Values->{mv_first_match} = $q->{mv_first_match}
409 if defined $q->{mv_first_match};
410 $::Values->{mv_searchspec} = $q->{mv_searchspec};
411 $::Values->{mv_raw_dict_look} = $q->{mv_raw_dict_look} || undef;
412 $::Values->{mv_dict_look} = $q->{mv_dict_look} || undef;
415 # Search for an item with glimpse or text engine
417 my($c,$more_matches,$pre_made) = @_;
418 #::logDebug('searching....');
420 #::logDebug("No search object");
421 return undef unless $Vend::Session->{search_params};
422 ($c, $more_matches) = @{$Vend::Session->{search_params}};
423 unless($c->{mv_cache_key}) {
424 #::logDebug("No cache key");
425 Vend::Scan::create_last_search($c);
426 $c->{mv_cache_key} = generate_key($Vend::Session->{last_search});
428 #::logDebug("Found search object=" . ::uneval($c));
430 elsif ($c->{mv_search_immediate}) {
431 unless($c->{mv_cache_key}) {
432 undef $c->{mv_search_immediate};
433 Vend::Scan::create_last_search($c);
434 $c->{mv_cache_key} = generate_key($Vend::Session->{last_search});
443 my ($p, $q, $matches);
446 $options{mv_session_id} = $c->{mv_session_id} || $Vend::SessionID;
447 if($c->{mv_more_matches}) {
448 #::logDebug("Found search object=" . ::uneval($c));
449 @options{qw/mv_cache_key mv_next_pointer mv_last_pointer mv_matchlimit mv_more_permanent/}
450 = split /:/, $c->{mv_more_matches};
451 $options{mv_more_id} = $c->{mv_more_id}
453 my $s = new Vend::Search %options;
454 #::logDebug("resulting search object=" . ::uneval($s));
455 $q = $s->more_matches();
461 # A text or glimpse search from here
463 parse_map($c) if defined $c->{mv_search_map};
465 if(defined $c->{mv_sql_query}) {
466 #::logDebug("found sql query in perform_search");
467 my $params = Vend::Interpolate::escape_scan(delete $c->{mv_sql_query}, \%CGI::values);
468 find_search_params($c, $params);
472 parse_profile_ref(\%options,$c);
475 foreach $p ( grep defined $c->{$_}, @ScanKeys) {
476 $c->{$Scan{$p}} = $c->{$p}
477 if ! defined $c->{$Scan{$p}};
479 foreach $p ( grep defined $c->{$_}, @Order) {
480 #::logDebug("Parsing $p mv_search_file");
481 if(defined $Parse{$p}) {
482 $options{$p} = &{$Parse{$p}}(\%options, $c->{$p})
485 $options{$p} = $c->{$p};
487 last if $options{$p} eq '-1' and $p eq 'mv_profile';
491 #::logDebug("Cache key: $options{mv_cache_key}");
492 if(! $options{mv_cache_key}) {
493 $options{mv_cache_key} = $c->{mv_search_label} ||
495 @{$options{mv_searchspec}},
496 @{$options{mv_search_field}},
497 @{$options{mv_search_file}},
499 #::logDebug("generated cache key: $options{mv_cache_key}");
502 #::logDebug("Options after parse: " . ::uneval(\%options));
505 if (defined $options{mv_searchtype} && $options{mv_searchtype} eq 'glimpse') {
506 undef $options{mv_searchtype} if ! $Vend::Cfg->{Glimpse};
511 $options{mv_return_all} = 1
512 if $options{mv_dict_look} and ! $options{mv_searchspec};
514 if (defined $pre_made) {
516 @{$q}{keys %options} = (values %options);
519 ! $options{mv_searchtype} && $::Variable->{MV_DEFAULT_SEARCH_DB}
520 or $options{mv_searchtype} =~ /db|sql/i
523 $q = new Vend::DbSearch %options;
525 elsif (! $options{mv_searchtype} or $options{mv_searchtype} eq 'text') {
526 $q = new Vend::TextSearch %options;
528 elsif ( $options{mv_searchtype} eq 'ref'){
529 $q = new Vend::RefSearch %options;
532 elsif ( $options{mv_searchtype} eq 'glimpse'){
533 $q = new Vend::Glimpse %options;
539 $q = "$Global::Variable->{$options{mv_searchtype}}"->new(%options);
542 ::logError("Search initialization for search type %s failed: %s",
543 $options{mv_searchtype}, $@);
545 ::display_special_page(
546 find_special_page('badsearch'),
547 errmsg('Search initialization failed')
553 if(defined $options{mv_return_spec}) {
554 $q->{matches} = scalar @{$q->{mv_searchspec}};
555 $q->{mv_results} = [ map { [ $_ ] } @{$q->{mv_searchspec}} ];
559 #::logDebug(::uneval($q));
563 if($q->{mv_list_only}) {
564 return $q->{mv_results};
573 my %scalar = (qw/ st 1 ra 1 co 1 os 1 sr 1 ml 1 ms 1/);
576 my ($parm, $val, $ary, $hash) = @_;
577 push(@$ary, "$parm=$val"), return
579 $hash->{$parm} = $val, return
582 if ! defined $hash->{$parm};
583 push @{$hash->{$parm}}, $val;
588 my($text, $ref, $table) = @_;
589 #::logDebug("sql_statement input=$text");
603 push_spec('fi', $table, $ary, $hash), push_spec('rt', $table, $ary, $hash)
605 unless "\L$table" eq 'glimpse';
609 # Strip possible leading stuff
610 $text =~ s/^\s*sq\s*=//;
613 $stmt = Vend::SQL_Parser->new($text, $ref);
615 if($@ and $text =~ s/^\s*sq\s*=(.*)//m) {
616 #::logDebug("failed first query, error=$@");
618 push @$ary, $text if $ary;
620 $stmt = Vend::SQL_Parser->new($text, $ref);
624 my $msg = ::errmsg("Bad SQL statement: %s\nQuery was: %s", $@, $text);
625 logError($msg) unless $Vend::Try;
632 #::logDebug("SQL statement=" . ::uneval($stmt));
634 my $update = $stmt->command();
635 #::logDebug("SQL command=$update");
636 undef $update if $update eq 'SELECT';
638 for($stmt->tables()) {
640 if($ref->{table_only}) {
643 #::logDebug("found table=$t");
646 my $db = Vend::Data::database_exists_ref($t);
648 $codename = $db->config('KEY') || 'code';
649 # Only for first table, what else can we do?
650 $nuhash ||= $db->config('NUMERIC') || undef;
651 push_spec( 'fi', $db->config('file'), $ary, $hash);
652 push_spec( 'rt', $t, $ary, $hash);
653 $stmt->verbatim_fields(1)
654 if $db->config('VERBATIM_FIELDS');
657 elsif ("\L$t" eq 'glimpse') {
660 push_spec('st', 'glimpse', $ary, $hash);
664 push_spec('fi', $t, $ary, $hash);
665 push_spec('rt', $t, $ary, $hash);
667 #::logDebug("t=$t obj=$_ db=$db nuhash=" . ::uneval($nuhash));
670 if(my $l = $stmt->limit()) {
671 #::logDebug("found limit=" . $l->limit());
672 push_spec('ml', $l->limit(), $ary, $hash);
673 if(my $fm = $l->offset()) {
674 #::logDebug("found offset=$fm");
675 push_spec('fm', $fm, $ary, $hash);
680 for($stmt->columns()) {
681 my $name = $_->name();
682 #::logDebug("found column=$name");
683 push_spec('un', 1, $ary, $hash) if $_->distinct() and ! $distincted++;
684 push_spec('rf', $name, $ary, $hash);
685 push_spec('hf', $_->as(), $ary, $hash);
686 last if $name eq '*';
687 #::logDebug("column name=" . $_->name() . " table=" . $_->table());
690 for my $v ($stmt->params()) {
691 my $val = $v->value();
692 my $type = $v->type();
693 #::logDebug(qq{found value="$val" type=$type});
694 push_spec('vv', $val, $ary, $hash);
695 push_spec('vt', $type, $ary, $hash);
700 @order = $stmt->order();
702 my $c = $_->column();
703 #::logDebug("found order column=$c");
704 push_spec('tf', $c, $ary, $hash);
705 my $d = $_->desc() ? 'fr' : 'f';
706 $d =~ s/f/n/ if exists $nuhash->{$c};
707 #::logDebug("found order sense=$d");
708 push_spec('to', $d, $ary, $hash);
711 #::logDebug("ary spec to this point=" . ::uneval($ary));
712 #::logDebug("hash spec to this point=" . ::uneval($hash));
714 @where = $stmt->where();
715 #::logDebug("where returned=" . ::uneval(\@where));
717 ## In a SQL query, we never want to drop out on empty string
718 push_spec('ms', 0, $ary, $hash);
720 push_spec( @$_, $ary, $hash );
724 push_spec('ra', 'yes', $ary, $hash);
727 if($hash->{sg} and ! $hash->{sr}) {
730 #::logDebug("sql_statement output=" . Vend::Util::uneval_it($hash)) if $hash;
731 return ($hash, $stmt) if $hash;
733 my $string = join "\n", @$ary;
734 #::logDebug("sql_statement output=$string");
741 my (@in) = split /\0/, $in;
743 my($var,$val) = split /=/, $_, 2;
744 $::Values->{$var} = $val;
750 return ($_[2] || []) unless $_[1];
751 my @fields = grep $_, split /\s*[,\0]\s*/, $_[1];
752 unshift(@fields, @{$_[2]}) if $_[2];
755 $_ = 'none' unless $_;
761 return ($_[2] || []) unless length($_[1]);
762 my @fields = grep /\S/, split /\s*[,\0]\s*/, $_[1];
763 unshift(@fields, @{$_[2]}) if $_[2];
768 if (! $_[0]->{mv_search_file} and defined ($col = column_index($_)) ) {
771 elsif ( $col = _find_field($_[0], $_) or defined $col ) {
775 ::logError( "Bad search column '%s=$col'" , $_ );
782 return ($_[2] || []) unless length $_[1];
783 my @fields = split /\s*[,\0]\s*/, $_[1];
784 unshift(@fields, @{$_[2]}) if $_[2];
788 next if $_[0]->{mv_verbatim_columns};
790 if (! defined $_[0]->{mv_search_file} and defined ($col = column_index($_)) ) {
793 elsif ( $col = _find_field($_[0], $_) or defined $col ) {
797 logError( "Bad search column '%s'" , $_ );
805 my ($file, $i, $line, @fields);
807 if($s->{mv_field_names}) {
808 @fields = @{$s->{mv_field_names}};
810 elsif(! defined $s->{mv_search_file}) {
813 elsif(ref $s->{mv_search_file}) {
814 $file = $s->{mv_search_file}->[0];
816 elsif($s->{mv_search_file}) {
817 $file = $s->{mv_search_file};
824 my $dir = $s->{mv_base_directory} || $Vend::Cfg->{ProductDir};
825 open (Vend::Scan::FIELDS, "< $dir/$file")
827 chomp($line = <Vend::Scan::FIELDS>);
830 $delim = quotemeta $1;
831 @fields = split /$delim/, $line;
832 close(Vend::Scan::FIELDS);
833 $s->{mv_field_names} = \@fields;
837 return $i if $_ eq $field;
844 return undef unless defined $_[1];
845 return undef unless $_[1] =~ m{^\S+$};
849 sub _verbatim_array {
850 return ($_[2] || undef) unless defined $_[1];
852 #::logDebug("receiving verbatim_array: " . ::uneval (\@_));
853 @fields = ref $_[1] ? @{$_[1]} : split /\0/, $_[1], -1;
854 @fields = ('') if ! @fields;
855 unshift(@fields, @{$_[2]}) if $_[2];
860 return ($_[2] || undef) unless defined $_[1];
862 @fields = ref $_[1] ? @{$_[1]} : split /\s*[,\0]\s*/, $_[1], -1;
863 unshift(@fields, @{$_[2]}) if $_[2];
868 return( defined($_[1]) && ($_[1] =~ /^[yYtT1]/));
872 defined $_[1] ? $_[1] : 0;
876 defined $_[1] ? $_[1] : '';
880 my ($junk, $param, $passed) = @_;
881 $passed = [] unless $passed;
882 my(@files) = grep /\S/, split /\s*[,\0]\s*/, $param, -1;
884 my $ok = allowed_file($_);
886 $ok = 1 if $_ eq $::Variable->{MV_SEARCH_FILE};
887 $ok = 1 if $::Scratch->{$_};
889 if(/^\w+$/ and ! $::Variable->{MV_DEFAULT_SEARCH_DB}) {
890 $_ = $Vend::Cfg->{Database}{$_}{file}
891 if defined $Vend::Cfg->{Database}{$_};
893 if ($ok and $Vend::Cfg->{NoSearch} and /$Vend::Cfg->{NoSearch}/) {
894 ::logError("Search of '%s' denied by NoSearch directive", $_);
897 push @$passed, $_ if $ok;
899 return $passed if @$passed;
903 sub _dir_security_scalar {
904 return undef if ! -d $_->[0];
908 sub _file_security_scalar {
909 my $result = _file_security(@_);
913 sub _scalar_or_array {
914 my(@fields) = split /\s*[,\0]\s*/, $_[1], -1;
917 $arg = [ $arg ] unless ref $arg;
918 unshift(@fields, @{$arg});
920 scalar @fields > 1 ? \@fields : (defined $fields[0] ? $fields[0] : '');
924 #::logDebug("_yes_array input=" . ::uneval(\@_));
925 my(@fields) = split /\s*[,\0]\s*/, $_[1];
927 unshift(@fields, ref $_[2] ? @{$_[2]} : $_[2]);
929 map { $_ = _yes('',$_) } @fields;
930 #::logDebug("_yes_array fields=" . ::uneval(\@fields));
935 my ($ref,$limit) = @_;
936 return undef unless defined $ref->{mv_dict_look};
937 $limit = -1 if $limit =~ /^[^-0-9]/;
938 $ref->{mv_dict_end} = $ref->{mv_dict_look};
939 substr($ref->{mv_dict_end},$limit,1) =~ s/(.)/chr(ord($1) + 1)/e;
946 return -1 if $val eq 'none' or $val eq 'all';
947 return int($val) || $::Variable->{MV_DEFAULT_MATCHLIMIT} || 50;