Bug 7684: (follow-up) fix UTF-8 encoding problems in CSV export
[koha.git] / C4 / Search.pm
index 8147e52..bec1f89 100644 (file)
@@ -36,7 +36,7 @@ use URI::Escape;
 use Business::ISBN;
 use MARC::Record;
 use MARC::Field;
-
+use utf8;
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
 
 # set the version for version checking
@@ -68,11 +68,10 @@ This module provides searching functions for Koha's bibliographic databases
   &searchResults
   &getRecords
   &buildQuery
-  &NZgetRecords
   &AddSearchHistory
   &GetDistinctValues
   &enabled_staff_search_views
-  &SimpleSearch
+  &PurgeSearchHistory
 );
 
 # make all your functions, whether exported or not;
@@ -113,6 +112,7 @@ sub FindDuplicate {
             $titleindex = 'title|exact';
             $authorindex = 'author|exact';
             $op = '&&';
+            $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
         } else {
             $titleindex = 'ti,ext';
             $authorindex = 'au,ext';
@@ -223,90 +223,85 @@ $template->param(result=>\@results);
 sub SimpleSearch {
     my ( $query, $offset, $max_results, $servers )  = @_;
 
-    if ( C4::Context->preference('NoZebra') ) {
-        my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
-        my $search_result =
-          (      $result->{hits}
-              && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
-        return ( undef, $search_result, scalar($result->{hits}) );
-    }
-    else {
-        return ( 'No query entered', undef, undef ) unless $query;
-        # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
-        my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
-        my @zoom_queries;
-        my @tmpresults;
-        my @zconns;
-        my $results = [];
-        my $total_hits = 0;
+    return ( 'No query entered', undef, undef ) unless $query;
+    # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
+    my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
+    my @zoom_queries;
+    my @tmpresults;
+    my @zconns;
+    my $results = [];
+    my $total_hits = 0;
 
-        my $QParser;
-        $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') && ! ($query =~ m/\w,\w|\w=\w/));
+    my $QParser;
+    $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') && ! ($query =~ m/\w,\w|\w=\w/));
+    if ($QParser) {
+        $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
+    }
 
-        # Initialize & Search Zebra
-        for ( my $i = 0 ; $i < @servers ; $i++ ) {
-            eval {
-                $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
-                if ($QParser) {
-                    $query =~ s/=/:/g;
-                    $QParser->parse( $query );
-                    $query = $QParser->target_syntax($servers[$i]);
-                    $zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
-                } else {
-                    $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
-                }
-                $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
-
-                # error handling
-                my $error =
-                    $zconns[$i]->errmsg() . " ("
-                  . $zconns[$i]->errcode() . ") "
-                  . $zconns[$i]->addinfo() . " "
-                  . $zconns[$i]->diagset();
-
-                return ( $error, undef, undef ) if $zconns[$i]->errcode();
-            };
-            if ($@) {
-
-                # caught a ZOOM::Exception
-                my $error =
-                    $@->message() . " ("
-                  . $@->code() . ") "
-                  . $@->addinfo() . " "
-                  . $@->diagset();
-                warn $error." for query: $query";
-                return ( $error, undef, undef );
+    # Initialize & Search Zebra
+    for ( my $i = 0 ; $i < @servers ; $i++ ) {
+        eval {
+            $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
+            if ($QParser) {
+                $query =~ s/=/:/g;
+                $QParser->parse( $query );
+                $query = $QParser->target_syntax($servers[$i]);
+                $zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
+            } else {
+                $query =~ s/:/=/g;
+                $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
             }
-        }
+            $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
 
-        _ZOOM_event_loop(
-            \@zconns,
-            \@tmpresults,
-            sub {
-                my ($i, $size) = @_;
-                my $first_record = defined($offset) ? $offset + 1 : 1;
-                my $hits = $tmpresults[ $i - 1 ]->size();
-                $total_hits += $hits;
-                my $last_record = $hits;
-                if ( defined $max_results && $offset + $max_results < $hits ) {
-                    $last_record = $offset + $max_results;
-                }
+            # error handling
+            my $error =
+                $zconns[$i]->errmsg() . " ("
+              . $zconns[$i]->errcode() . ") "
+              . $zconns[$i]->addinfo() . " "
+              . $zconns[$i]->diagset();
 
-                for my $j ( $first_record .. $last_record ) {
-                    my $record =
-                      $tmpresults[ $i - 1 ]->record( $j - 1 )->raw()
-                      ;    # 0 indexed
-                    push @{$results}, $record;
-                }
+            return ( $error, undef, undef ) if $zconns[$i]->errcode();
+        };
+        if ($@) {
+
+            # caught a ZOOM::Exception
+            my $error =
+                $@->message() . " ("
+              . $@->code() . ") "
+              . $@->addinfo() . " "
+              . $@->diagset();
+            warn $error." for query: $query";
+            return ( $error, undef, undef );
+        }
+    }
+
+    _ZOOM_event_loop(
+        \@zconns,
+        \@tmpresults,
+        sub {
+            my ($i, $size) = @_;
+            my $first_record = defined($offset) ? $offset + 1 : 1;
+            my $hits = $tmpresults[ $i - 1 ]->size();
+            $total_hits += $hits;
+            my $last_record = $hits;
+            if ( defined $max_results && $offset + $max_results < $hits ) {
+                $last_record = $offset + $max_results;
             }
-        );
 
-        foreach my $zoom_query (@zoom_queries) {
-            $zoom_query->destroy();
+            for my $j ( $first_record .. $last_record ) {
+                my $record =
+                  $tmpresults[ $i - 1 ]->record( $j - 1 )->raw()
+                  ;    # 0 indexed
+                push @{$results}, $record;
+            }
         }
+    );
 
-        return ( undef, $results, $total_hits );
+    foreach my $zoom_query (@zoom_queries) {
+        $zoom_query->destroy();
     }
+
+    return ( undef, $results, $total_hits );
 }
 
 =head2 getRecords
@@ -756,7 +751,7 @@ sub _remove_stopwords {
     my @stopwords_removed;
 
     # phrase and exact-qualified indexes shouldn't have stopwords removed
-    if ( $index !~ m/phr|ext/ ) {
+    if ( $index !~ m/,(phr|ext)/ ) {
 
 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
 #       we use IsAlpha unicode definition, to deal correctly with diacritics.
@@ -896,8 +891,11 @@ sub _build_weighted_query {
         $weighted_query .= " $index,ext,r1=\"$operand\"";    # exact index
           #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
         $weighted_query .= " or $index,phr,r3=\"$operand\"";    # phrase index
-        $weighted_query .=
-          " or $index,rt,wrdl,r3=\"$operand\"";    # word list index
+        $weighted_query .= " or $index,wrdl,r6=\"$operand\"";    # word list index
+        $weighted_query .= " or $index,wrdl,fuzzy,r8=\"$operand\""
+          if $fuzzy_enabled;    # add fuzzy, word list
+        $weighted_query .= " or $index,wrdl,rt,r9=\"$stemmed_operand\""
+          if ( $stemming and $stemmed_operand );    # add stemming, right truncation
     }
 
     $weighted_query .= "))";                       # close rank specification
@@ -958,6 +956,7 @@ sub getIndexes{
                     'Corporate-name-heading',
                     'Corporate-name-see',
                     'Corporate-name-seealso',
+                    'Country-publication',
                     'ctype',
                     'date-entered-on-file',
                     'Date-of-acquisition',
@@ -1117,7 +1116,7 @@ on authority data).
 =cut
 
 sub _handle_exploding_index {
-    my ($QParser, $struct, $filter, $params, $negate, $server) = @_;
+    my ($QParser, $filter, $params, $negate, $server) = @_;
     my $index = $filter;
     my $term = join(' ', @$params);
 
@@ -1127,7 +1126,7 @@ sub _handle_exploding_index {
 
     my $codesubfield = $marcflavour eq 'UNIMARC' ? '5' : 'w';
     my $wantedcodes = '';
-    my @subqueries = ( "su:\"$term\"");
+    my @subqueries = ( "\@attr 1=Subject \@attr 4=1 \"$term\"");
     my ($error, $results, $total_hits) = SimpleSearch( "he:$term", undef, undef, [ "authorityserver" ] );
     foreach my $auth (@$results) {
         my $record = MARC::Record->new_from_usmarc($auth);
@@ -1142,12 +1141,11 @@ sub _handle_exploding_index {
             }
             foreach my $reference (@references) {
                 my $codes = $reference->subfield($codesubfield);
-                push @subqueries, 'su:"' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '"' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
+                push @subqueries, '@attr 1=Subject @attr 4=1 "' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '"' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
             }
         }
     }
-    my $query = '(' x scalar(@subqueries) . join(') || ', @subqueries) . ')';
-    warn $query;
+    my $query = ' @or ' x (scalar(@subqueries) - 1) . join(' ', @subqueries);
     return $query;
 }
 
@@ -1179,35 +1177,53 @@ sub parseQuery {
 
     my $QParser;
     $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') || $query =~ s/^qp=//);
-    undef $QParser if ($query =~ m/^(ccl=|pqf=|cql=)/ || grep (/\w,\w|\w=\w/, @operands) );
+    undef $QParser if ($query =~ m/^(ccl=|pqf=|cql=)/ || grep (/\w,\w|\w=\w/, @operands, @indexes) );
+    undef $QParser if (scalar @limits > 0);
 
     if ($QParser)
     {
+        $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
         $query = '';
         for ( my $ii = 0 ; $ii <= @operands ; $ii++ ) {
             next unless $operands[$ii];
             $query .= $operators[ $ii - 1 ] eq 'or' ? ' || ' : ' && '
               if ($query);
-            $query .=
-              ( $indexes[$ii] ? "$indexes[$ii]:" : '' ) . $operands[$ii];
+            if ( $indexes[$ii] =~ m/su-/ ) {
+                $query .= $indexes[$ii] . '(' . $operands[$ii] . ')';
+            }
+            else {
+                $query .=
+                  ( $indexes[$ii] ? "$indexes[$ii]:" : '' ) . $operands[$ii];
+            }
         }
         foreach my $limit (@limits) {
         }
-        foreach my $modifier (@sort_by) {
-            $query .= " #$modifier";
+        if ( scalar(@sort_by) > 0 ) {
+            my $modifier_re =
+              '#(' . join( '|', @{ $QParser->modifiers } ) . ')';
+            $query =~ s/$modifier_re//g;
+            foreach my $modifier (@sort_by) {
+                $query .= " #$modifier";
+            }
         }
 
         $query_desc = $query;
+        $query_desc =~ s/\s+/ /g;
         if ( C4::Context->preference("QueryWeightFields") ) {
         }
-        $QParser->add_bib1_filter_map( 'biblioserver', 'su-br', { 'callback' => \&_handle_exploding_index });
-        $QParser->add_bib1_filter_map( 'biblioserver', 'su-na', { 'callback' => \&_handle_exploding_index });
-        $QParser->add_bib1_filter_map( 'biblioserver', 'su-rl', { 'callback' => \&_handle_exploding_index });
-        $QParser->parse( $query );
+        $QParser->add_bib1_filter_map( 'su-br' => 'biblioserver' =>
+              { 'target_syntax_callback' => \&_handle_exploding_index } );
+        $QParser->add_bib1_filter_map( 'su-na' => 'biblioserver' =>
+              { 'target_syntax_callback' => \&_handle_exploding_index } );
+        $QParser->add_bib1_filter_map( 'su-rl' => 'biblioserver' =>
+              { 'target_syntax_callback' => \&_handle_exploding_index } );
+        $QParser->parse($query);
         $operands[0] = "pqf=" . $QParser->target_syntax('biblioserver');
-# TODO: once we are using QueryParser, all this special case code for
-#       exploded search indexes will be replaced by a callback to
-#       _handle_exploding_index
+    }
+    else {
+        require Koha::QueryParser::Driver::PQF;
+        my $modifier_re = '#(' . join( '|', @{Koha::QueryParser::Driver::PQF->modifiers}) . ')';
+        s/$modifier_re//g for @operands;
     }
 
     return ( $operators, \@operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc);
@@ -1250,14 +1266,6 @@ sub buildQuery {
     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
     my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
 
-    # no stemming/weight/fuzzy in NoZebra
-    if ( C4::Context->preference("NoZebra") ) {
-        $stemming         = 0;
-        $weight_fields    = 0;
-        $fuzzy_enabled    = 0;
-       $auto_truncation  = 0;
-    }
-
     my $query        = $operands[0];
     my $simple_query = $operands[0];
 
@@ -1291,17 +1299,17 @@ sub buildQuery {
         if ( @limits ) {
             $q .= ' and '.join(' and ', @limits);
         }
-        return ( undef, $q, $q, "q=ccl=$q", $q, '', '', '', '', 'ccl' );
+        return ( undef, $q, $q, "q=ccl=".uri_escape($q), $q, '', '', '', '', 'ccl' );
     }
     if ( $query =~ /^cql=/ ) {
-        return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
+        return ( undef, $', $', "q=cql=".uri_escape($'), $', '', '', '', '', 'cql' );
     }
     if ( $query =~ /^pqf=/ ) {
         if ($query_desc) {
-            $query_cgi = "q=$query_desc";
+            $query_cgi = "q=".uri_escape($query_desc);
         } else {
             $query_desc = $';
-            $query_cgi = "q=pqf=$'";
+            $query_cgi = "q=pqf=".uri_escape($');
         }
         return ( undef, $', $', $query_cgi, $query_desc, '', '', '', '', 'pqf' );
     }
@@ -1379,7 +1387,7 @@ sub buildQuery {
 
                 # Set default structure attribute (word list)
                 my $struct_attr = q{};
-                unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl|nb|ns)/ ) {
+                unless ( $indexes_set || !$index || $index =~ /,(st-|phr|ext|wrdl)/ || $index =~ /^(nb|ns)$/ ) {
                     $struct_attr = ",wrdl";
                 }
 
@@ -1397,7 +1405,7 @@ sub buildQuery {
                 }
 
                 if ($auto_truncation){
-                                       unless ( $index =~ /(st-|phr|ext)/ ) {
+                        unless ( $index =~ /,(st-|phr|ext)/ ) {
                                                #FIXME only valid with LTR scripts
                                                $operand=join(" ",map{
                                                                                        (index($_,"*")>0?"$_":"$_*")
@@ -1473,9 +1481,9 @@ sub buildQuery {
                         $query     .= " $operators[$i-1] ";
                         $query     .= " $index_plus " unless $indexes_set;
                         $query     .= " $operand";
-                        $query_cgi .= "&op=$operators[$i-1]";
-                        $query_cgi .= "&idx=$index" if $index;
-                        $query_cgi .= "&q=$operands[$i]" if $operands[$i];
+                        $query_cgi .= "&op=".uri_escape($operators[$i-1]);
+                        $query_cgi .= "&idx=".uri_escape($index) if $index;
+                        $query_cgi .= "&q=".uri_escape($operands[$i]) if $operands[$i];
                         $query_desc .=
                           " $operators[$i-1] $index_plus $operands[$i]";
                     }
@@ -1485,8 +1493,8 @@ sub buildQuery {
                         $query      .= " and ";
                         $query      .= "$index_plus " unless $indexes_set;
                         $query      .= "$operand";
-                        $query_cgi  .= "&op=and&idx=$index" if $index;
-                        $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
+                        $query_cgi  .= "&op=and&idx=".uri_escape($index) if $index;
+                        $query_cgi  .= "&q=".uri_escape($operands[$i]) if $operands[$i];
                         $query_desc .= " and $index_plus $operands[$i]";
                     }
                 }
@@ -1498,8 +1506,8 @@ sub buildQuery {
                     $query .= " $index_plus " unless $indexes_set;
                     $query .= $operand;
                     $query_desc .= " $index_plus $operands[$i]";
-                    $query_cgi  .= "&idx=$index" if $index;
-                    $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
+                    $query_cgi  .= "&idx=".uri_escape($index) if $index;
+                    $query_cgi  .= "&q=".uri_escape($operands[$i]) if $operands[$i];
                     $previous_operand = 1;
                 }
             }    #/if $operands
@@ -1808,7 +1816,7 @@ sub searchResults {
         my $onloan_count          = 0;
         my $longoverdue_count     = 0;
         my $other_count           = 0;
-        my $wthdrawn_count        = 0;
+        my $withdrawn_count        = 0;
         my $itemlost_count        = 0;
         my $hideatopac_count      = 0;
         my $itembinding_count     = 0;
@@ -1895,7 +1903,7 @@ sub searchResults {
                 # is item on the reserve shelf?
                 my $reservestatus = '';
 
-                unless ($item->{wthdrawn}
+                unless ($item->{withdrawn}
                         || $item->{itemlost}
                         || $item->{damaged}
                         || $item->{notforloan}
@@ -1918,42 +1926,36 @@ sub searchResults {
                 }
 
                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
-                if (   $item->{wthdrawn}
+                if (   $item->{withdrawn}
                     || $item->{itemlost}
                     || $item->{damaged}
                     || $item->{notforloan}
                     || $reservestatus eq 'Waiting'
                     || ($transfertwhen ne ''))
                 {
-                    $wthdrawn_count++        if $item->{wthdrawn};
+                    $withdrawn_count++        if $item->{withdrawn};
                     $itemlost_count++        if $item->{itemlost};
                     $itemdamaged_count++     if $item->{damaged};
                     $item_in_transit_count++ if $transfertwhen ne '';
                     $item_onhold_count++     if $reservestatus eq 'Waiting';
-                    $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
-
-                    # can place hold on item ?
-                    if ( !$item->{itemlost} ) {
-                        if ( !$item->{wthdrawn} ){
-                            if ( $item->{damaged} ){
-                                if ( C4::Context->preference('AllowHoldsOnDamagedItems') ){
-                                    # can place a hold on a damaged item if AllowHoldsOnDamagedItems is true
-                                    if ( ( !$item->{notforloan} || $item->{notforloan} < 0 ) ){
-                                        # item is either for loan or has notforloan < 0
-                                        $can_place_holds = 1;
-                                    }
-                                }
-                            } elsif ( $item->{notforloan} < 0 ) {
-                                # item is not damaged and notforloan is < 0
-                                $can_place_holds = 1;
-                            }
-                        }
-                    }
+                    $item->{status} = $item->{withdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
+
+                    # can place a hold on a item if
+                    # not lost nor withdrawn
+                    # not damaged unless AllowHoldsOnDamagedItems is true
+                    # item is either for loan or on order (notforloan < 0)
+                    $can_place_holds = 1
+                      if (
+                           !$item->{itemlost}
+                        && !$item->{withdrawn}
+                        && ( !$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems') )
+                        && ( !$item->{notforloan} || $item->{notforloan} < 0 )
+                      );
 
                     $other_count++;
 
                     my $key = $prefix . $item->{status};
-                    foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
+                    foreach (qw(withdrawn itemlost damaged branchname itemcallnumber)) {
                         $other_items->{$key}->{$_} = $item->{$_};
                     }
                     $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
@@ -2025,7 +2027,7 @@ sub searchResults {
         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
         $oldbiblio->{othercount}           = $other_count;
         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
-        $oldbiblio->{wthdrawncount}        = $wthdrawn_count;
+        $oldbiblio->{withdrawncount}        = $withdrawn_count;
         $oldbiblio->{itemlostcount}        = $itemlost_count;
         $oldbiblio->{damagedcount}         = $itemdamaged_count;
         $oldbiblio->{intransitcount}       = $item_in_transit_count;
@@ -2154,602 +2156,6 @@ sub SearchAcquisitions{
     $qdataacquisitions->finish;
     return \@loopacquisitions;
 }
-#----------------------------------------------------------------------
-#
-# Non-Zebra GetRecords#
-#----------------------------------------------------------------------
-
-=head2 NZgetRecords
-
-  NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
-
-=cut
-
-sub NZgetRecords {
-    my (
-        $query,            $simple_query, $sort_by_ref,    $servers_ref,
-        $results_per_page, $offset,       $expanded_facet, $branches,
-        $query_type,       $scan
-    ) = @_;
-    warn "query =$query" if $DEBUG;
-    my $result = NZanalyse($query);
-    warn "results =$result" if $DEBUG;
-    return ( undef,
-        NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
-        undef );
-}
-
-=head2 NZanalyse
-
-  NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
-  the list is built from an inverted index in the nozebra SQL table
-  note that title is here only for convenience : the sorting will be very fast when requested on title
-  if the sorting is requested on something else, we will have to reread all results, and that may be longer.
-
-=cut
-
-sub NZanalyse {
-    my ( $string, $server ) = @_;
-#     warn "---------"       if $DEBUG;
-    warn " NZanalyse" if $DEBUG;
-#     warn "---------"       if $DEBUG;
-
- # $server contains biblioserver or authorities, depending on what we search on.
- #warn "querying : $string on $server";
-    $server = 'biblioserver' unless $server;
-
-# if we have a ", replace the content to discard temporarily any and/or/not inside
-    my $commacontent;
-    if ( $string =~ /"/ ) {
-        $string =~ s/"(.*?)"/__X__/;
-        $commacontent = $1;
-        warn "commacontent : $commacontent" if $DEBUG;
-    }
-
-# split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
-# then, call again NZanalyse with $left and $right
-# (recursive until we find a leaf (=> something without and/or/not)
-# delete repeated operator... Would then go in infinite loop
-    while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
-    }
-
-    #process parenthesis before.
-    if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
-        my $left     = $1;
-        my $right    = $4;
-        my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
-        warn
-"dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
-          if $DEBUG;
-        my $leftresult = NZanalyse( $left, $server );
-        if ($operator) {
-            my $rightresult = NZanalyse( $right, $server );
-
-            # OK, we have the results for right and left part of the query
-            # depending of operand, intersect, union or exclude both lists
-            # to get a result list
-            if ( $operator eq ' and ' ) {
-                return NZoperatorAND($leftresult,$rightresult);
-            }
-            elsif ( $operator eq ' or ' ) {
-
-                # just merge the 2 strings
-                return $leftresult . $rightresult;
-            }
-            elsif ( $operator eq ' not ' ) {
-                return NZoperatorNOT($leftresult,$rightresult);
-            }
-        }
-        else {
-# this error is impossible, because of the regexp that isolate the operand, but just in case...
-            return $leftresult;
-        }
-    }
-    warn "string :" . $string if $DEBUG;
-    my $left = "";
-    my $right = "";
-    my $operator = "";
-    if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
-        $left     = $1;
-        $right    = $3;
-        $operator = lc($2);    # FIXME: and/or/not are operators, not operands
-    }
-    warn "no parenthesis. left : $left operator: $operator right: $right"
-      if $DEBUG;
-
-    # it's not a leaf, we have a and/or/not
-    if ($operator) {
-
-        # reintroduce comma content if needed
-        $right =~ s/__X__/"$commacontent"/ if $commacontent;
-        $left  =~ s/__X__/"$commacontent"/ if $commacontent;
-        warn "node : $left / $operator / $right\n" if $DEBUG;
-        my $leftresult  = NZanalyse( $left,  $server );
-        my $rightresult = NZanalyse( $right, $server );
-        warn " leftresult : $leftresult" if $DEBUG;
-        warn " rightresult : $rightresult" if $DEBUG;
-        # OK, we have the results for right and left part of the query
-        # depending of operand, intersect, union or exclude both lists
-        # to get a result list
-        if ( $operator eq ' and ' ) {
-            return NZoperatorAND($leftresult,$rightresult);
-        }
-        elsif ( $operator eq ' or ' ) {
-
-            # just merge the 2 strings
-            return $leftresult . $rightresult;
-        }
-        elsif ( $operator eq ' not ' ) {
-            return NZoperatorNOT($leftresult,$rightresult);
-        }
-        else {
-
-# this error is impossible, because of the regexp that isolate the operand, but just in case...
-            die "error : operand unknown : $operator for $string";
-        }
-
-        # it's a leaf, do the real SQL query and return the result
-    }
-    else {
-        $string =~ s/__X__/"$commacontent"/ if $commacontent;
-        $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
-        #remove trailing blank at the beginning
-        $string =~ s/^ //g;
-        warn "leaf:$string" if $DEBUG;
-
-        # parse the string in in operator/operand/value again
-        my $left = "";
-        my $operator = "";
-        my $right = "";
-        if ($string =~ /(.*)(>=|<=)(.*)/) {
-            $left     = $1;
-            $operator = $2;
-            $right    = $3;
-        } else {
-            $left = $string;
-        }
-#         warn "handling leaf... left:$left operator:$operator right:$right"
-#           if $DEBUG;
-        unless ($operator) {
-            if ($string =~ /(.*)(>|<|=)(.*)/) {
-                $left     = $1;
-                $operator = $2;
-                $right    = $3;
-                warn
-    "handling unless (operator)... left:$left operator:$operator right:$right"
-                if $DEBUG;
-            } else {
-                $left = $string;
-            }
-        }
-        my $results;
-
-# strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
-        $left =~ s/ .*$//;
-
-        # automatic replace for short operators
-        $left = 'title'            if $left =~ '^ti$';
-        $left = 'author'           if $left =~ '^au$';
-        $left = 'publisher'        if $left =~ '^pb$';
-        $left = 'subject'          if $left =~ '^su$';
-        $left = 'koha-Auth-Number' if $left =~ '^an$';
-        $left = 'keyword'          if $left =~ '^kw$';
-        $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
-        warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
-        my $dbh = C4::Context->dbh;
-        if ( $operator && $left ne 'keyword' ) {
-            #do a specific search
-            $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
-            my $sth = $dbh->prepare(
-"SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
-            );
-            warn "$left / $operator / $right\n" if $DEBUG;
-
-            # split each word, query the DB and build the biblionumbers result
-            #sanitizing leftpart
-            $left =~ s/^\s+|\s+$//;
-            foreach ( split / /, $right ) {
-                my $biblionumbers;
-                $_ =~ s/^\s+|\s+$//;
-                next unless $_;
-                warn "EXECUTE : $server, $left, $_" if $DEBUG;
-                $sth->execute( $server, $left, $_ )
-                  or warn "execute failed: $!";
-                while ( my ( $line, $value ) = $sth->fetchrow ) {
-
-# if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
-# otherwise, fill the result
-                    $biblionumbers .= $line
-                      unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
-                    warn "result : $value "
-                      . ( $right  =~ /\d/ ) . "=="
-                      . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
-                }
-
-# do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
-                if ($results) {
-                    warn "NZAND" if $DEBUG;
-                    $results = NZoperatorAND($biblionumbers,$results);
-                } else {
-                    $results = $biblionumbers;
-                }
-            }
-        }
-        else {
-      #do a complete search (all indexes), if index='kw' do complete search too.
-            my $sth = $dbh->prepare(
-"SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
-            );
-
-            # split each word, query the DB and build the biblionumbers result
-            foreach ( split / /, $string ) {
-                next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
-                warn "search on all indexes on $_" if $DEBUG;
-                my $biblionumbers;
-                next unless $_;
-                $sth->execute( $server, $_ );
-                while ( my $line = $sth->fetchrow ) {
-                    $biblionumbers .= $line;
-                }
-
-# do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
-                if ($results) {
-                    $results = NZoperatorAND($biblionumbers,$results);
-                }
-                else {
-                    warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
-                    $results = $biblionumbers;
-                }
-            }
-        }
-        warn "return : $results for LEAF : $string" if $DEBUG;
-        return $results;
-    }
-    warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
-}
-
-sub NZoperatorAND{
-    my ($rightresult, $leftresult)=@_;
-
-    my @leftresult = split /;/, $leftresult;
-    warn " @leftresult / $rightresult \n" if $DEBUG;
-
-    #             my @rightresult = split /;/,$leftresult;
-    my $finalresult;
-
-# parse the left results, and if the biblionumber exist in the right result, save it in finalresult
-# the result is stored twice, to have the same weight for AND than OR.
-# example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
-# result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
-    foreach (@leftresult) {
-        my $value = $_;
-        my $countvalue;
-        ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
-        if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
-            $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
-            $finalresult .=
-                "$value-$countvalue;$value-$countvalue;";
-        }
-    }
-    warn "NZAND DONE : $finalresult \n" if $DEBUG;
-    return $finalresult;
-}
-
-sub NZoperatorOR{
-    my ($rightresult, $leftresult)=@_;
-    return $rightresult.$leftresult;
-}
-
-sub NZoperatorNOT{
-    my ($leftresult, $rightresult)=@_;
-
-    my @leftresult = split /;/, $leftresult;
-
-    #             my @rightresult = split /;/,$leftresult;
-    my $finalresult;
-    foreach (@leftresult) {
-        my $value=$_;
-        $value=$1 if $value=~m/(.*)-\d+$/;
-        unless ($rightresult =~ "$value-") {
-            $finalresult .= "$_;";
-        }
-    }
-    return $finalresult;
-}
-
-=head2 NZorder
-
-  $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
-
-  TODO :: Description
-
-=cut
-
-sub NZorder {
-    my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
-    warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
-
-    # order title asc by default
-    #     $ordering = '1=36 <i' unless $ordering;
-    $results_per_page = 20 unless $results_per_page;
-    $offset           = 0  unless $offset;
-    my $dbh = C4::Context->dbh;
-
-    #
-    # order by POPULARITY
-    #
-    if ( $ordering =~ /popularity/ ) {
-        my %result;
-        my %popularity;
-
-        # popularity is not in MARC record, it's builded from a specific query
-        my $sth =
-          $dbh->prepare("select sum(issues) from items where biblionumber=?");
-        foreach ( split /;/, $biblionumbers ) {
-            my ( $biblionumber, $title ) = split /,/, $_;
-            $result{$biblionumber} = GetMarcBiblio($biblionumber);
-            $sth->execute($biblionumber);
-            my $popularity = $sth->fetchrow || 0;
-
-# hint : the key is popularity.title because we can have
-# many results with the same popularity. In this case, sub-ordering is done by title
-# we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
-# (un-frequent, I agree, but we won't forget anything that way ;-)
-            $popularity{ sprintf( "%10d", $popularity ) . $title
-                  . $biblionumber } = $biblionumber;
-        }
-
-    # sort the hash and return the same structure as GetRecords (Zebra querying)
-        my $result_hash;
-        my $numbers = 0;
-        if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
-            foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{ $popularity{$key} }->as_usmarc();
-            }
-        }
-        else {                                    # sort popularity ASC
-            foreach my $key ( sort ( keys %popularity ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{ $popularity{$key} }->as_usmarc();
-            }
-        }
-        my $finalresult = ();
-        $result_hash->{'hits'}         = $numbers;
-        $finalresult->{'biblioserver'} = $result_hash;
-        return $finalresult;
-
-        #
-        # ORDER BY author
-        #
-    }
-    elsif ( $ordering =~ /author/ ) {
-        my %result;
-        foreach ( split /;/, $biblionumbers ) {
-            my ( $biblionumber, $title ) = split /,/, $_;
-            my $record = GetMarcBiblio($biblionumber);
-            my $author;
-            if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
-                $author = $record->subfield( '200', 'f' );
-                $author = $record->subfield( '700', 'a' ) unless $author;
-            }
-            else {
-                $author = $record->subfield( '100', 'a' );
-            }
-
-# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
-# and we don't want to get only 1 result for each of them !!!
-            $result{ $author . $biblionumber } = $record;
-        }
-
-    # sort the hash and return the same structure as GetRecords (Zebra querying)
-        my $result_hash;
-        my $numbers = 0;
-        if ( $ordering eq 'author_za' || $ordering eq 'author_dsc' ) {    # sort by author desc
-            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{$key}->as_usmarc();
-            }
-        }
-        else {                               # sort by author ASC
-            foreach my $key ( sort ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{$key}->as_usmarc();
-            }
-        }
-        my $finalresult = ();
-        $result_hash->{'hits'}         = $numbers;
-        $finalresult->{'biblioserver'} = $result_hash;
-        return $finalresult;
-
-        #
-        # ORDER BY callnumber
-        #
-    }
-    elsif ( $ordering =~ /callnumber/ ) {
-        my %result;
-        foreach ( split /;/, $biblionumbers ) {
-            my ( $biblionumber, $title ) = split /,/, $_;
-            my $record = GetMarcBiblio($biblionumber);
-            my $callnumber;
-            my $frameworkcode = GetFrameworkCode($biblionumber);
-            my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField(  'items.itemcallnumber', $frameworkcode);
-               ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
-                unless $callnumber_tag;
-            if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
-                $callnumber = $record->subfield( '200', 'f' );
-            } else {
-                $callnumber = $record->subfield( '100', 'a' );
-            }
-
-# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
-# and we don't want to get only 1 result for each of them !!!
-            $result{ $callnumber . $biblionumber } = $record;
-        }
-
-    # sort the hash and return the same structure as GetRecords (Zebra querying)
-        my $result_hash;
-        my $numbers = 0;
-        if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
-            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{$key}->as_usmarc();
-            }
-        }
-        else {                                     # sort by title ASC
-            foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{$key}->as_usmarc();
-            }
-        }
-        my $finalresult = ();
-        $result_hash->{'hits'}         = $numbers;
-        $finalresult->{'biblioserver'} = $result_hash;
-        return $finalresult;
-    }
-    elsif ( $ordering =~ /pubdate/ ) {             #pub year
-        my %result;
-        foreach ( split /;/, $biblionumbers ) {
-            my ( $biblionumber, $title ) = split /,/, $_;
-            my $record = GetMarcBiblio($biblionumber);
-            my ( $publicationyear_tag, $publicationyear_subfield ) =
-              GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
-            my $publicationyear =
-              $record->subfield( $publicationyear_tag,
-                $publicationyear_subfield );
-
-# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
-# and we don't want to get only 1 result for each of them !!!
-            $result{ $publicationyear . $biblionumber } = $record;
-        }
-
-    # sort the hash and return the same structure as GetRecords (Zebra querying)
-        my $result_hash;
-        my $numbers = 0;
-        if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
-            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{$key}->as_usmarc();
-            }
-        }
-        else {                                 # sort by pub year ASC
-            foreach my $key ( sort ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] =
-                  $result{$key}->as_usmarc();
-            }
-        }
-        my $finalresult = ();
-        $result_hash->{'hits'}         = $numbers;
-        $finalresult->{'biblioserver'} = $result_hash;
-        return $finalresult;
-
-        #
-        # ORDER BY title
-        #
-    }
-    elsif ( $ordering =~ /title/ ) {
-
-# the title is in the biblionumbers string, so we just need to build a hash, sort it and return
-        my %result;
-        foreach ( split /;/, $biblionumbers ) {
-            my ( $biblionumber, $title ) = split /,/, $_;
-
-# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
-# and we don't want to get only 1 result for each of them !!!
-# hint & speed improvement : we can order without reading the record
-# so order, and read records only for the requested page !
-            $result{ $title . $biblionumber } = $biblionumber;
-        }
-
-    # sort the hash and return the same structure as GetRecords (Zebra querying)
-        my $result_hash;
-        my $numbers = 0;
-        if ( $ordering eq 'title_az' ) {    # sort by title desc
-            foreach my $key ( sort ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
-            }
-        }
-        else {                              # sort by title ASC
-            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
-                $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
-            }
-        }
-
-        # limit the $results_per_page to result size if it's more
-        $results_per_page = $numbers - 1 if $numbers < $results_per_page;
-
-        # for the requested page, replace biblionumber by the complete record
-        # speed improvement : avoid reading too much things
-        for (
-            my $counter = $offset ;
-            $counter <= $offset + $results_per_page ;
-            $counter++
-          )
-        {
-            $result_hash->{'RECORDS'}[$counter] =
-              GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
-        }
-        my $finalresult = ();
-        $result_hash->{'hits'}         = $numbers;
-        $finalresult->{'biblioserver'} = $result_hash;
-        return $finalresult;
-    }
-    else {
-
-#
-# order by ranking
-#
-# we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
-        my %result;
-        my %count_ranking;
-        foreach ( split /;/, $biblionumbers ) {
-            my ( $biblionumber, $title ) = split /,/, $_;
-            $title =~ /(.*)-(\d)/;
-
-            # get weight
-            my $ranking = $2;
-
-# note that we + the ranking because ranking is calculated on weight of EACH term requested.
-# if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
-# biblio N has ranking = 6
-            $count_ranking{$biblionumber} += $ranking;
-        }
-
-# build the result by "inverting" the count_ranking hash
-# hing : as usual, we don't order by ranking only, to avoid having only 1 result for each rank. We build an hash on concat(ranking,biblionumber) instead
-#         warn "counting";
-        foreach ( keys %count_ranking ) {
-            $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
-        }
-
-    # sort the hash and return the same structure as GetRecords (Zebra querying)
-        my $result_hash;
-        my $numbers = 0;
-        foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
-            $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
-        }
-
-        # limit the $results_per_page to result size if it's more
-        $results_per_page = $numbers - 1 if $numbers < $results_per_page;
-
-        # for the requested page, replace biblionumber by the complete record
-        # speed improvement : avoid reading too much things
-        for (
-            my $counter = $offset ;
-            $counter <= $offset + $results_per_page ;
-            $counter++
-          )
-        {
-            $result_hash->{'RECORDS'}[$counter] =
-              GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
-              if $result_hash->{'RECORDS'}[$counter];
-        }
-        my $finalresult = ();
-        $result_hash->{'hits'}         = $numbers;
-        $finalresult->{'biblioserver'} = $result_hash;
-        return $finalresult;
-    }
-}
 
 =head2 enabled_staff_search_views
 
@@ -2806,6 +2212,13 @@ sub GetSearchHistory{
     return  $sth->fetchall_hashref({});
 }
 
+sub PurgeSearchHistory{
+    my ($pSearchhistory)=@_;
+    my $dbh = C4::Context->dbh;
+    my $sth = $dbh->prepare("DELETE FROM search_history WHERE time < DATE_SUB( NOW(), INTERVAL ? DAY )");
+    $sth->execute($pSearchhistory) or die $dbh->errstr;
+}
+
 =head2 z3950_search_args
 
 $arrayref = z3950_search_args($matchpoints)