Merge remote branch 'kc/new/enh/bug_1962' into kcmaster
[koha.git] / C4 / Search.pm
1 package C4::Search;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18 use strict;
19 #use warnings; FIXME - Bug 2505
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha;      # getFacets
24 use Lingua::Stem;
25 use C4::Search::PazPar2;
26 use XML::Simple;
27 use C4::Dates qw(format_date);
28 use C4::XSLT;
29 use C4::Branch;
30 use C4::Reserves;    # CheckReserves
31 use C4::Debug;
32 use URI::Escape;
33
34 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
35
36 # set the version for version checking
37 BEGIN {
38     $VERSION = 3.01;
39     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
40 }
41
42 =head1 NAME
43
44 C4::Search - Functions for searching the Koha catalog.
45
46 =head1 SYNOPSIS
47
48 See opac/opac-search.pl or catalogue/search.pl for example of usage
49
50 =head1 DESCRIPTION
51
52 This module provides searching functions for Koha's bibliographic databases
53
54 =head1 FUNCTIONS
55
56 =cut
57
58 @ISA    = qw(Exporter);
59 @EXPORT = qw(
60   &FindDuplicate
61   &SimpleSearch
62   &searchResults
63   &getRecords
64   &buildQuery
65   &NZgetRecords
66   &AddSearchHistory
67   &GetDistinctValues
68   &BiblioAddAuthorities
69 );
70 #FIXME: i had to add BiblioAddAuthorities here because in Biblios.pm it caused circular dependencies (C4::Search uses C4::Biblio, and BiblioAddAuthorities uses SimpleSearch from C4::Search)
71
72 # make all your functions, whether exported or not;
73
74 =head2 FindDuplicate
75
76 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
77
78 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
79
80 =cut
81
82 sub FindDuplicate {
83     my ($record) = @_;
84     my $dbh = C4::Context->dbh;
85     my $result = TransformMarcToKoha( $dbh, $record, '' );
86     my $sth;
87     my $query;
88     my $search;
89     my $type;
90     my ( $biblionumber, $title );
91
92     # search duplicate on ISBN, easy and fast..
93     # ... normalize first
94     if ( $result->{isbn} ) {
95         $result->{isbn} =~ s/\(.*$//;
96         $result->{isbn} =~ s/\s+$//;
97         $query = "isbn=$result->{isbn}";
98     }
99     else {
100         $result->{title} =~ s /\\//g;
101         $result->{title} =~ s /\"//g;
102         $result->{title} =~ s /\(//g;
103         $result->{title} =~ s /\)//g;
104
105         # FIXME: instead of removing operators, could just do
106         # quotes around the value
107         $result->{title} =~ s/(and|or|not)//g;
108         $query = "ti,ext=$result->{title}";
109         $query .= " and itemtype=$result->{itemtype}"
110           if ( $result->{itemtype} );
111         if   ( $result->{author} ) {
112             $result->{author} =~ s /\\//g;
113             $result->{author} =~ s /\"//g;
114             $result->{author} =~ s /\(//g;
115             $result->{author} =~ s /\)//g;
116
117             # remove valid operators
118             $result->{author} =~ s/(and|or|not)//g;
119             $query .= " and au,ext=$result->{author}";
120         }
121     }
122
123     # FIXME: add error handling
124     my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
125     my @results;
126     foreach my $possible_duplicate_record (@$searchresults) {
127         my $marcrecord =
128           MARC::Record->new_from_usmarc($possible_duplicate_record);
129         my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
130
131         # FIXME :: why 2 $biblionumber ?
132         if ($result) {
133             push @results, $result->{'biblionumber'};
134             push @results, $result->{'title'};
135         }
136     }
137     return @results;
138 }
139
140 =head2 SimpleSearch
141
142 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
143
144 This function provides a simple search API on the bibliographic catalog
145
146 =over 2
147
148 =item C<input arg:>
149
150     * $query can be a simple keyword or a complete CCL query
151     * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
152     * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
153     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
154
155
156 =item C<Output:>
157
158     * $error is a empty unless an error is detected
159     * \@results is an array of records.
160     * $total_hits is the number of hits that would have been returned with no limit
161
162 =item C<usage in the script:>
163
164 =back
165
166 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
167
168 if (defined $error) {
169     $template->param(query_error => $error);
170     warn "error: ".$error;
171     output_html_with_http_headers $input, $cookie, $template->output;
172     exit;
173 }
174
175 my $hits = scalar @$marcresults;
176 my @results;
177
178 for my $i (0..$hits) {
179     my %resultsloop;
180     my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
181     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
182
183     #build the hash for the template.
184     $resultsloop{title}           = $biblio->{'title'};
185     $resultsloop{subtitle}        = $biblio->{'subtitle'};
186     $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
187     $resultsloop{author}          = $biblio->{'author'};
188     $resultsloop{publishercode}   = $biblio->{'publishercode'};
189     $resultsloop{publicationyear} = $biblio->{'publicationyear'};
190
191     push @results, \%resultsloop;
192 }
193
194 $template->param(result=>\@results);
195
196 =cut
197
198 sub SimpleSearch {
199     my ( $query, $offset, $max_results, $servers )  = @_;
200
201     if ( C4::Context->preference('NoZebra') ) {
202         my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
203         my $search_result =
204           (      $result->{hits}
205               && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
206         return ( undef, $search_result, scalar($result->{hits}) );
207     }
208     else {
209         # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
210         my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
211         my @results;
212         my @zoom_queries;
213         my @tmpresults;
214         my @zconns;
215         my $total_hits;
216         return ( "No query entered", undef, undef ) unless $query;
217
218         # Initialize & Search Zebra
219         for ( my $i = 0 ; $i < @servers ; $i++ ) {
220             eval {
221                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
222                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
223                 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
224
225                 # error handling
226                 my $error =
227                     $zconns[$i]->errmsg() . " ("
228                   . $zconns[$i]->errcode() . ") "
229                   . $zconns[$i]->addinfo() . " "
230                   . $zconns[$i]->diagset();
231
232                 return ( $error, undef, undef ) if $zconns[$i]->errcode();
233             };
234             if ($@) {
235
236                 # caught a ZOOM::Exception
237                 my $error =
238                     $@->message() . " ("
239                   . $@->code() . ") "
240                   . $@->addinfo() . " "
241                   . $@->diagset();
242                 warn $error;
243                 return ( $error, undef, undef );
244             }
245         }
246         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
247             my $event = $zconns[ $i - 1 ]->last_event();
248             if ( $event == ZOOM::Event::ZEND ) {
249
250                 my $first_record = defined( $offset ) ? $offset+1 : 1;
251                 my $hits = $tmpresults[ $i - 1 ]->size();
252                 $total_hits += $hits;
253                 my $last_record = $hits;
254                 if ( defined $max_results && $offset + $max_results < $hits ) {
255                     $last_record  = $offset + $max_results;
256                 }
257
258                 for my $j ( $first_record..$last_record ) {
259                     my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
260                     push @results, $record;
261                 }
262             }
263         }
264
265         foreach my $result (@tmpresults) {
266             $result->destroy();
267         }
268         foreach my $zoom_query (@zoom_queries) {
269             $zoom_query->destroy();
270         }
271
272         return ( undef, \@results, $total_hits );
273     }
274 }
275
276 =head2 getRecords
277
278 ( undef, $results_hashref, \@facets_loop ) = getRecords (
279
280         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
281         $results_per_page, $offset,       $expanded_facet, $branches,
282         $query_type,       $scan
283     );
284
285 The all singing, all dancing, multi-server, asynchronous, scanning,
286 searching, record nabbing, facet-building
287
288 See verbse embedded documentation.
289
290 =cut
291
292 sub getRecords {
293     my (
294         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
295         $results_per_page, $offset,       $expanded_facet, $branches,
296         $query_type,       $scan
297     ) = @_;
298
299     my @servers = @$servers_ref;
300     my @sort_by = @$sort_by_ref;
301
302     # Initialize variables for the ZOOM connection and results object
303     my $zconn;
304     my @zconns;
305     my @results;
306     my $results_hashref = ();
307
308     # Initialize variables for the faceted results objects
309     my $facets_counter = ();
310     my $facets_info    = ();
311     my $facets         = getFacets();
312     my $facets_maxrecs = C4::Context->preference('maxRecordsForFacets')||20;
313
314     my @facets_loop;    # stores the ref to array of hashes for template facets loop
315
316     ### LOOP THROUGH THE SERVERS
317     for ( my $i = 0 ; $i < @servers ; $i++ ) {
318         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
319
320 # perform the search, create the results objects
321 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
322         my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
323
324         #$query_to_use = $simple_query if $scan;
325         warn $simple_query if ( $scan and $DEBUG );
326
327         # Check if we've got a query_type defined, if so, use it
328         eval {
329             if ($query_type) {
330                 if ($query_type =~ /^ccl/) {
331                     $query_to_use =~ s/\:/\=/g;    # change : to = last minute (FIXME)
332                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
333                 } elsif ($query_type =~ /^cql/) {
334                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
335                 } elsif ($query_type =~ /^pqf/) {
336                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
337                 } else {
338                     warn "Unknown query_type '$query_type'.  Results undetermined.";
339                 }
340             } elsif ($scan) {
341                     $results[$i] = $zconns[$i]->scan(  new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
342             } else {
343                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
344             }
345         };
346         if ($@) {
347             warn "WARNING: query problem with $query_to_use " . $@;
348         }
349
350         # Concatenate the sort_by limits and pass them to the results object
351         # Note: sort will override rank
352         my $sort_by;
353         foreach my $sort (@sort_by) {
354             if ( $sort eq "author_az" ) {
355                 $sort_by .= "1=1003 <i ";
356             }
357             elsif ( $sort eq "author_za" ) {
358                 $sort_by .= "1=1003 >i ";
359             }
360             elsif ( $sort eq "popularity_asc" ) {
361                 $sort_by .= "1=9003 <i ";
362             }
363             elsif ( $sort eq "popularity_dsc" ) {
364                 $sort_by .= "1=9003 >i ";
365             }
366             elsif ( $sort eq "call_number_asc" ) {
367                 $sort_by .= "1=20  <i ";
368             }
369             elsif ( $sort eq "call_number_dsc" ) {
370                 $sort_by .= "1=20 >i ";
371             }
372             elsif ( $sort eq "pubdate_asc" ) {
373                 $sort_by .= "1=31 <i ";
374             }
375             elsif ( $sort eq "pubdate_dsc" ) {
376                 $sort_by .= "1=31 >i ";
377             }
378             elsif ( $sort eq "acqdate_asc" ) {
379                 $sort_by .= "1=32 <i ";
380             }
381             elsif ( $sort eq "acqdate_dsc" ) {
382                 $sort_by .= "1=32 >i ";
383             }
384             elsif ( $sort eq "title_az" ) {
385                 $sort_by .= "1=4 <i ";
386             }
387             elsif ( $sort eq "title_za" ) {
388                 $sort_by .= "1=4 >i ";
389             }
390             else {
391                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
392             }
393         }
394         if ($sort_by) {
395             if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
396                 warn "WARNING sort $sort_by failed";
397             }
398         }
399     }    # finished looping through servers
400
401     # The big moment: asynchronously retrieve results from all servers
402     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
403         my $ev = $zconns[ $i - 1 ]->last_event();
404         if ( $ev == ZOOM::Event::ZEND ) {
405             next unless $results[ $i - 1 ];
406             my $size = $results[ $i - 1 ]->size();
407             if ( $size > 0 ) {
408                 my $results_hash;
409
410                 # loop through the results
411                 $results_hash->{'hits'} = $size;
412                 my $times;
413                 if ( $offset + $results_per_page <= $size ) {
414                     $times = $offset + $results_per_page;
415                 }
416                 else {
417                     $times = $size;
418                 }
419                 for ( my $j = $offset ; $j < $times ; $j++ ) {
420                     my $records_hash;
421                     my $record;
422
423                     ## Check if it's an index scan
424                     if ($scan) {
425                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
426
427                  # here we create a minimal MARC record and hand it off to the
428                  # template just like a normal result ... perhaps not ideal, but
429                  # it works for now
430                         my $tmprecord = MARC::Record->new();
431                         $tmprecord->encoding('UTF-8');
432                         my $tmptitle;
433                         my $tmpauthor;
434
435                 # the minimal record in author/title (depending on MARC flavour)
436                         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
437                             $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
438                             $tmprecord->append_fields($tmptitle);
439                         } else {
440                             $tmptitle  = MARC::Field->new('245',' ',' ', a => $term,);
441                             $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
442                             $tmprecord->append_fields($tmptitle);
443                             $tmprecord->append_fields($tmpauthor);
444                         }
445                         $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
446                     }
447
448                     # not an index scan
449                     else {
450                         $record = $results[ $i - 1 ]->record($j)->raw();
451
452                         # warn "RECORD $j:".$record;
453                         $results_hash->{'RECORDS'}[$j] = $record;
454                     }
455
456                 }
457                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
458
459                 # Fill the facets while we're looping, but only for the biblioserver and not for a scan
460                 if ( !$scan && $servers[ $i - 1 ] =~ /biblioserver/ ) {
461
462                     my $jmax = $size>$facets_maxrecs? $facets_maxrecs: $size;
463
464                     for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
465                         ($facets->[$k]) or next;
466                         my @fcodes = @{$facets->[$k]->{'tags'}};
467                         my $sfcode = $facets->[$k]->{'subfield'};
468
469                                 for ( my $j = 0 ; $j < $jmax ; $j++ ) {
470                                     my $render_record = $results[ $i - 1 ]->record($j)->render();
471                             my @used_datas = ();
472
473                             foreach my $fcode (@fcodes) {
474
475                                 # avoid first line
476                                 my $field_pattern = '\n'.$fcode.' ([^\n]+)';
477                                 my @field_tokens = ( $render_record =~ /$field_pattern/g ) ;
478
479                                 foreach my $field_token (@field_tokens) {
480                                     my $subfield_pattern = '\$'.$sfcode.' ([^\$]+)';
481                                     my @subfield_values = ( $field_token =~ /$subfield_pattern/g );
482
483                                     foreach my $subfield_value (@subfield_values) {
484
485                                         my $data = $subfield_value;
486                                         $data =~ s/^\s+//; # trim left
487                                         $data =~ s/\s+$//; # trim right
488
489                                         unless ( $data ~~ @used_datas ) {
490                                             $facets_counter->{ $facets->[$k]->{'link_value'} }->{$data}++;
491                                             push @used_datas, $data;
492                                         }
493                                     } # subfields
494                                 } # fields
495                             } # field codes
496                         } # records
497
498                         $facets_info->{ $facets->[$k]->{'link_value'} }->{'label_value'} = $facets->[$k]->{'label_value'};
499                         $facets_info->{ $facets->[$k]->{'link_value'} }->{'expanded'} = $facets->[$k]->{'expanded'};
500                     } # facets
501                 }
502                 # End PROGILONE
503             }
504
505             # warn "connection ", $i-1, ": $size hits";
506             # warn $results[$i-1]->record(0)->render() if $size > 0;
507
508             # BUILD FACETS
509             if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
510                 for my $link_value (
511                     sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
512                         keys %$facets_counter )
513                 {
514                     my $expandable;
515                     my $number_of_facets;
516                     my @this_facets_array;
517                     for my $one_facet (
518                         sort {
519                              $facets_counter->{$link_value}->{$b}
520                          <=> $facets_counter->{$link_value}->{$a}
521                         } keys %{ $facets_counter->{$link_value} }
522                       )
523                     {
524                         $number_of_facets++;
525                         if (   ( $number_of_facets < 6 )
526                             || ( $expanded_facet eq $link_value )
527                             || ( $facets_info->{$link_value}->{'expanded'} ) )
528                         {
529
530                       # Sanitize the link value ), ( will cause errors with CCL,
531                             my $facet_link_value = $one_facet;
532                             $facet_link_value =~ s/(\(|\))/ /g;
533
534                             # fix the length that will display in the label,
535                             my $facet_label_value = $one_facet;
536                             my $facet_max_length =
537                                 C4::Context->preference('FacetLabelTruncationLength') || 20;
538                             $facet_label_value =
539                               substr( $one_facet, 0, $facet_max_length ) . "..."
540                                 if length($facet_label_value) > $facet_max_length;
541
542                             # if it's a branch, label by the name, not the code,
543                             if ( $link_value =~ /branch/ ) {
544                                                                 if (defined $branches
545                                                                         && ref($branches) eq "HASH"
546                                                                         && defined $branches->{$one_facet}
547                                                                         && ref ($branches->{$one_facet}) eq "HASH")
548                                                                 {
549                                         $facet_label_value =
550                                                 $branches->{$one_facet}->{'branchname'};
551                                                                 }
552                                                                 else {
553                                                                         $facet_label_value = "*";
554                                                                 }
555                             }
556
557                             # but we're down with the whole label being in the link's title.
558                             push @this_facets_array, {
559                                 facet_count       => $facets_counter->{$link_value}->{$one_facet},
560                                 facet_label_value => $facet_label_value,
561                                 facet_title_value => $one_facet,
562                                 facet_link_value  => $facet_link_value,
563                                 type_link_value   => $link_value,
564                             };
565                         }
566                     }
567
568                     # handle expanded option
569                     unless ( $facets_info->{$link_value}->{'expanded'} ) {
570                         $expandable = 1
571                           if ( ( $number_of_facets > 6 )
572                             && ( $expanded_facet ne $link_value ) );
573                     }
574                     push @facets_loop, {
575                         type_link_value => $link_value,
576                         type_id         => $link_value . "_id",
577                         "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1,
578                         facets     => \@this_facets_array,
579                         expandable => $expandable,
580                         expand     => $link_value,
581                     } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
582                 }
583             }
584         }
585     }
586     return ( undef, $results_hashref, \@facets_loop );
587 }
588
589 sub pazGetRecords {
590     my (
591         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
592         $results_per_page, $offset,       $expanded_facet, $branches,
593         $query_type,       $scan
594     ) = @_;
595
596     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
597     $paz->init();
598     $paz->search($simple_query);
599     sleep 1;   # FIXME: WHY?
600
601     # do results
602     my $results_hashref = {};
603     my $stats = XMLin($paz->stat);
604     my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
605
606     # for a grouped search result, the number of hits
607     # is the number of groups returned; 'bib_hits' will have
608     # the total number of bibs.
609     $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
610     $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
611
612     HIT: foreach my $hit (@{ $results->{'hit'} }) {
613         my $recid = $hit->{recid}->[0];
614
615         my $work_title = $hit->{'md-work-title'}->[0];
616         my $work_author;
617         if (exists $hit->{'md-work-author'}) {
618             $work_author = $hit->{'md-work-author'}->[0];
619         }
620         my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
621
622         my $result_group = {};
623         $result_group->{'group_label'} = $group_label;
624         $result_group->{'group_merge_key'} = $recid;
625
626         my $count = 1;
627         if (exists $hit->{count}) {
628             $count = $hit->{count}->[0];
629         }
630         $result_group->{'group_count'} = $count;
631
632         for (my $i = 0; $i < $count; $i++) {
633             # FIXME -- may need to worry about diacritics here
634             my $rec = $paz->record($recid, $i);
635             push @{ $result_group->{'RECORDS'} }, $rec;
636         }
637
638         push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
639     }
640
641     # pass through facets
642     my $termlist_xml = $paz->termlist('author,subject');
643     my $terms = XMLin($termlist_xml, forcearray => 1);
644     my @facets_loop = ();
645     #die Dumper($results);
646 #    foreach my $list (sort keys %{ $terms->{'list'} }) {
647 #        my @facets = ();
648 #        foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
649 #            push @facets, {
650 #                facet_label_value => $facet->{'name'}->[0],
651 #            };
652 #        }
653 #        push @facets_loop, ( {
654 #            type_label => $list,
655 #            facets => \@facets,
656 #        } );
657 #    }
658
659     return ( undef, $results_hashref, \@facets_loop );
660 }
661
662 # STOPWORDS
663 sub _remove_stopwords {
664     my ( $operand, $index ) = @_;
665     my @stopwords_removed;
666
667     # phrase and exact-qualified indexes shouldn't have stopwords removed
668     if ( $index !~ m/phr|ext/ ) {
669
670 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
671 #       we use IsAlpha unicode definition, to deal correctly with diacritics.
672 #       otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
673 #       is a stopword, we'd get "çon" and wouldn't find anything...
674 #
675                 foreach ( keys %{ C4::Context->stopwords } ) {
676                         next if ( $_ =~ /(and|or|not)/ );    # don't remove operators
677                         if ( my ($matched) = ($operand =~
678                                 /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
679                         {
680                                 $operand =~ s/\Q$matched\E/ /gi;
681                                 push @stopwords_removed, $_;
682                         }
683                 }
684         }
685     return ( $operand, \@stopwords_removed );
686 }
687
688 # TRUNCATION
689 sub _detect_truncation {
690     my ( $operand, $index ) = @_;
691     my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
692         @regexpr );
693     $operand =~ s/^ //g;
694     my @wordlist = split( /\s/, $operand );
695     foreach my $word (@wordlist) {
696         if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
697             push @rightlefttruncated, $word;
698         }
699         elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
700             push @lefttruncated, $word;
701         }
702         elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
703             push @righttruncated, $word;
704         }
705         elsif ( index( $word, "*" ) < 0 ) {
706             push @nontruncated, $word;
707         }
708         else {
709             push @regexpr, $word;
710         }
711     }
712     return (
713         \@nontruncated,       \@righttruncated, \@lefttruncated,
714         \@rightlefttruncated, \@regexpr
715     );
716 }
717
718 # STEMMING
719 sub _build_stemmed_operand {
720     my ($operand,$lang) = @_;
721     require Lingua::Stem::Snowball ;
722     my $stemmed_operand;
723
724     # If operand contains a digit, it is almost certainly an identifier, and should
725     # not be stemmed.  This is particularly relevant for ISBNs and ISSNs, which
726     # can contain the letter "X" - for example, _build_stemmend_operand would reduce
727     # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
728     # results (e.g., "23 x 29 cm." from the 300$c).  Bug 2098.
729     return $operand if $operand =~ /\d/;
730
731 # FIXME: the locale should be set based on the user's language and/or search choice
732     #warn "$lang";
733     my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
734                                                encoding => "UTF-8" );
735
736     my @words = split( / /, $operand );
737     my @stems = $stemmer->stem(\@words);
738     for my $stem (@stems) {
739         $stemmed_operand .= "$stem";
740         $stemmed_operand .= "?"
741           unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
742         $stemmed_operand .= " ";
743     }
744     warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
745     return $stemmed_operand;
746 }
747
748 # FIELD WEIGHTING
749 sub _build_weighted_query {
750
751 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
752 # pretty well but could work much better if we had a smarter query parser
753     my ( $operand, $stemmed_operand, $index ) = @_;
754     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
755     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
756     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")        || 0;
757
758     my $weighted_query .= "(rk=(";    # Specifies that we're applying rank
759
760     # Keyword, or, no index specified
761     if ( ( $index eq 'kw' ) || ( !$index ) ) {
762         $weighted_query .=
763           "Title-cover,ext,r1=\"$operand\"";    # exact title-cover
764         $weighted_query .= " or ti,ext,r2=\"$operand\"";    # exact title
765         $weighted_query .= " or ti,phr,r3=\"$operand\"";    # phrase title
766           #$weighted_query .= " or any,ext,r4=$operand";               # exact any
767           #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
768         $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
769           if $fuzzy_enabled;    # add fuzzy, word list
770         $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
771           if ( $stemming and $stemmed_operand )
772           ;                     # add stemming, right truncation
773         $weighted_query .= " or wrdl,r9=\"$operand\"";
774
775         # embedded sorting: 0 a-z; 1 z-a
776         # $weighted_query .= ") or (sort1,aut=1";
777     }
778
779     # Barcode searches should skip this process
780     elsif ( $index eq 'bc' ) {
781         $weighted_query .= "bc=\"$operand\"";
782     }
783
784     # Authority-number searches should skip this process
785     elsif ( $index eq 'an' ) {
786         $weighted_query .= "an=\"$operand\"";
787     }
788
789     # If the index already has more than one qualifier, wrap the operand
790     # in quotes and pass it back (assumption is that the user knows what they
791     # are doing and won't appreciate us mucking up their query
792     elsif ( $index =~ ',' ) {
793         $weighted_query .= " $index=\"$operand\"";
794     }
795
796     #TODO: build better cases based on specific search indexes
797     else {
798         $weighted_query .= " $index,ext,r1=\"$operand\"";    # exact index
799           #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
800         $weighted_query .= " or $index,phr,r3=\"$operand\"";    # phrase index
801         $weighted_query .=
802           " or $index,rt,wrdl,r3=\"$operand\"";    # word list index
803     }
804
805     $weighted_query .= "))";                       # close rank specification
806     return $weighted_query;
807 }
808
809 =head2 getIndexes
810
811 Return an array with available indexes.
812
813 =cut
814
815 sub getIndexes{
816     my @indexes = (
817                     # biblio indexes
818                     'ab',
819                     'Abstract',
820                     'acqdate',
821                     'allrecords',
822                     'an',
823                     'Any',
824                     'at',
825                     'au',
826                     'aub',
827                     'aud',
828                     'audience',
829                     'auo',
830                     'aut',
831                     'Author',
832                     'Author-in-order ',
833                     'Author-personal-bibliography',
834                     'Authority-Number',
835                     'authtype',
836                     'bc',
837                     'biblionumber',
838                     'bio',
839                     'biography',
840                     'callnum',
841                     'cfn',
842                     'Chronological-subdivision',
843                     'cn-bib-source',
844                     'cn-bib-sort',
845                     'cn-class',
846                     'cn-item',
847                     'cn-prefix',
848                     'cn-suffix',
849                     'cpn',
850                     'Code-institution',
851                     'Conference-name',
852                     'Conference-name-heading',
853                     'Conference-name-see',
854                     'Conference-name-seealso',
855                     'Content-type',
856                     'Control-number',
857                     'copydate',
858                     'Corporate-name',
859                     'Corporate-name-heading',
860                     'Corporate-name-see',
861                     'Corporate-name-seealso',
862                     'ctype',
863                     'date-entered-on-file',
864                     'Date-of-acquisition',
865                     'Date-of-publication',
866                     'Dewey-classification',
867                     'extent',
868                     'fic',
869                     'fiction',
870                     'Form-subdivision',
871                     'format',
872                     'Geographic-subdivision',
873                     'he',
874                     'Heading',
875                     'Heading-use-main-or-added-entry',
876                     'Heading-use-series-added-entry ',
877                     'Heading-use-subject-added-entry',
878                     'Host-item',
879                     'id-other',
880                     'Illustration-code',
881                     'ISBN',
882                     'isbn',
883                     'ISSN',
884                     'issn',
885                     'itemtype',
886                     'kw',
887                     'Koha-Auth-Number',
888                     'l-format',
889                     'language',
890                     'lc-card',
891                     'LC-card-number',
892                     'lcn',
893                     'llength',
894                     'ln',
895                     'Local-classification',
896                     'Local-number',
897                     'Match-heading',
898                     'Match-heading-see-from',
899                     'Material-type',
900                     'mc-itemtype',
901                     'mc-rtype',
902                     'mus',
903                     'name',
904                     'Name-geographic',
905                     'Name-geographic-heading',
906                     'Name-geographic-see',
907                     'Name-geographic-seealso',
908                     'nb',
909                     'Note',
910                     'notes',
911                     'ns',
912                     'nt',
913                     'pb',
914                     'Personal-name',
915                     'Personal-name-heading',
916                     'Personal-name-see',
917                     'Personal-name-seealso',
918                     'pl',
919                     'Place-publication',
920                     'pn',
921                     'popularity',
922                     'pubdate',
923                     'Publisher',
924                     'Record-control-number',
925                     'rcn',
926                     'Record-type',
927                     'rtype',
928                     'se',
929                     'See',
930                     'See-also',
931                     'sn',
932                     'Stock-number',
933                     'su',
934                     'Subject',
935                     'Subject-heading-thesaurus',
936                     'Subject-name-personal',
937                     'Subject-subdivision',
938                     'Summary',
939                     'Suppress',
940                     'su-geo',
941                     'su-na',
942                     'su-to',
943                     'su-ut',
944                     'ut',
945                     'Term-genre-form',
946                     'Term-genre-form-heading',
947                     'Term-genre-form-see',
948                     'Term-genre-form-seealso',
949                     'ti',
950                     'Title',
951                     'Title-cover',
952                     'Title-series',
953                     'Title-uniform',
954                     'Title-uniform-heading',
955                     'Title-uniform-see',
956                     'Title-uniform-seealso',
957                     'totalissues',
958                     'yr',
959
960                     # items indexes
961                     'acqsource',
962                     'barcode',
963                     'bc',
964                     'branch',
965                     'ccode',
966                     'classification-source',
967                     'cn-sort',
968                     'coded-location-qualifier',
969                     'copynumber',
970                     'damaged',
971                     'datelastborrowed',
972                     'datelastseen',
973                     'holdingbranch',
974                     'homebranch',
975                     'issues',
976                     'item',
977                     'itemnumber',
978                     'itype',
979                     'Local-classification',
980                     'location',
981                     'lost',
982                     'materials-specified',
983                     'mc-ccode',
984                     'mc-itype',
985                     'mc-loc',
986                     'notforloan',
987                     'onloan',
988                     'price',
989                     'renewals',
990                     'replacementprice',
991                     'replacementpricedate',
992                     'reserves',
993                     'restricted',
994                     'stack',
995                     'uri',
996                     'withdrawn',
997
998                     # subject related
999                   );
1000
1001     return \@indexes;
1002 }
1003
1004 =head2 buildQuery
1005
1006 ( $error, $query,
1007 $simple_query, $query_cgi,
1008 $query_desc, $limit,
1009 $limit_cgi, $limit_desc,
1010 $stopwords_removed, $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1011
1012 Build queries and limits in CCL, CGI, Human,
1013 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
1014
1015 See verbose embedded documentation.
1016
1017
1018 =cut
1019
1020 sub buildQuery {
1021     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1022
1023     warn "---------\nEnter buildQuery\n---------" if $DEBUG;
1024
1025     # dereference
1026     my @operators = $operators ? @$operators : ();
1027     my @indexes   = $indexes   ? @$indexes   : ();
1028     my @operands  = $operands  ? @$operands  : ();
1029     my @limits    = $limits    ? @$limits    : ();
1030     my @sort_by   = $sort_by   ? @$sort_by   : ();
1031
1032     my $stemming         = C4::Context->preference("QueryStemming")        || 0;
1033     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
1034     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
1035     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
1036     my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
1037
1038     # no stemming/weight/fuzzy in NoZebra
1039     if ( C4::Context->preference("NoZebra") ) {
1040         $stemming         = 0;
1041         $weight_fields    = 0;
1042         $fuzzy_enabled    = 0;
1043         $auto_truncation  = 0;
1044     }
1045
1046     my $query        = $operands[0];
1047     my $simple_query = $operands[0];
1048
1049     # initialize the variables we're passing back
1050     my $query_cgi;
1051     my $query_desc;
1052     my $query_type;
1053
1054     my $limit;
1055     my $limit_cgi;
1056     my $limit_desc;
1057
1058     my $stopwords_removed;    # flag to determine if stopwords have been removed
1059
1060     my $cclq;
1061     my $cclindexes = getIndexes();
1062     if( $query !~ /\s*ccl=/ ){
1063         for my $index (@$cclindexes){
1064             if($query =~ /($index)(,?\w)*[:=]/){
1065                 $cclq = 1;
1066             }
1067         }
1068         $query = "ccl=$query" if($cclq);
1069     }
1070
1071 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1072 # DIAGNOSTIC ONLY!!
1073     if ( $query =~ /^ccl=/ ) {
1074         my $q=$';
1075         # This is needed otherwise ccl= and &limit won't work together, and
1076         # this happens when selecting a subject on the opac-detail page
1077         if (@limits) {
1078             $q .= ' and '.join(' and ', @limits);
1079         }
1080         return ( undef, $q, $q, "q=ccl=$q", $q, '', '', '', '', 'ccl' );
1081     }
1082     if ( $query =~ /^cql=/ ) {
1083         return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
1084     }
1085     if ( $query =~ /^pqf=/ ) {
1086         return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
1087     }
1088
1089     # pass nested queries directly
1090     # FIXME: need better handling of some of these variables in this case
1091     # Nested queries aren't handled well and this implementation is flawed and causes users to be
1092     # unable to search for anything containing () commenting out, will be rewritten for 3.4.0
1093 #    if ( $query =~ /(\(|\))/ ) {
1094 #        return (
1095 #            undef,              $query, $simple_query, $query_cgi,
1096 #            $query,             $limit, $limit_cgi,    $limit_desc,
1097 #            $stopwords_removed, 'ccl'
1098 #        );
1099 #    }
1100
1101 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1102 # query operands and indexes and add stemming, truncation, field weighting, etc.
1103 # Once we do so, we'll end up with a value in $query, just like if we had an
1104 # incoming $query from the user
1105     else {
1106         $query = ""
1107           ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1108         my $previous_operand
1109           ;    # a flag used to keep track if there was a previous query
1110                # if there was, we can apply the current operator
1111                # for every operand
1112         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1113
1114             # COMBINE OPERANDS, INDEXES AND OPERATORS
1115             if ( $operands[$i] ) {
1116                 $operands[$i]=~s/^\s+//;
1117
1118               # A flag to determine whether or not to add the index to the query
1119                 my $indexes_set;
1120
1121 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
1122                 if ( $operands[$i] =~ /(:|=)/ || $scan ) {
1123                     $weight_fields    = 0;
1124                     $stemming         = 0;
1125                     $remove_stopwords = 0;
1126                 }
1127                 my $operand = $operands[$i];
1128                 my $index   = $indexes[$i];
1129
1130                 # Add index-specific attributes
1131                 # Date of Publication
1132                 if ( $index eq 'yr' ) {
1133                     $index .= ",st-numeric";
1134                     $indexes_set++;
1135                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1136                 }
1137
1138                 # Date of Acquisition
1139                 elsif ( $index eq 'acqdate' ) {
1140                     $index .= ",st-date-normalized";
1141                     $indexes_set++;
1142                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1143                 }
1144                 # ISBN,ISSN,Standard Number, don't need special treatment
1145                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
1146                     $indexes_set++;
1147                     (
1148                         $stemming,      $auto_truncation,
1149                         $weight_fields, $fuzzy_enabled,
1150                         $remove_stopwords
1151                     ) = ( 0, 0, 0, 0, 0 );
1152
1153                 }
1154
1155                 if(not $index){
1156                     $index = 'kw';
1157                 }
1158
1159                 # Set default structure attribute (word list)
1160                 my $struct_attr = q{};
1161                 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
1162                     $struct_attr = ",wrdl";
1163                 }
1164
1165                 # Some helpful index variants
1166                 my $index_plus       = $index . $struct_attr . ':';
1167                 my $index_plus_comma = $index . $struct_attr . ',';
1168
1169                 # Remove Stopwords
1170                 if ($remove_stopwords) {
1171                     ( $operand, $stopwords_removed ) =
1172                       _remove_stopwords( $operand, $index );
1173                     warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
1174                     warn "REMOVED STOPWORDS: @$stopwords_removed"
1175                       if ( $stopwords_removed && $DEBUG );
1176                 }
1177
1178                 if ($auto_truncation){
1179                                         unless ( $index =~ /(st-|phr|ext)/ ) {
1180                                                 #FIXME only valid with LTR scripts
1181                                                 $operand=join(" ",map{
1182                                                                                         (index($_,"*")>0?"$_":"$_*")
1183                                                                                          }split (/\s+/,$operand));
1184                                                 warn $operand if $DEBUG;
1185                                         }
1186                                 }
1187
1188                 # Detect Truncation
1189                 my $truncated_operand;
1190                 my( $nontruncated, $righttruncated, $lefttruncated,
1191                     $rightlefttruncated, $regexpr
1192                 ) = _detect_truncation( $operand, $index );
1193                 warn
1194 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1195                   if $DEBUG;
1196
1197                 # Apply Truncation
1198                 if (
1199                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
1200                     scalar(@$rightlefttruncated) > 0 )
1201                 {
1202
1203                # Don't field weight or add the index to the query, we do it here
1204                     $indexes_set = 1;
1205                     undef $weight_fields;
1206                     my $previous_truncation_operand;
1207                     if (scalar @$nontruncated) {
1208                         $truncated_operand .= "$index_plus @$nontruncated ";
1209                         $previous_truncation_operand = 1;
1210                     }
1211                     if (scalar @$righttruncated) {
1212                         $truncated_operand .= "and " if $previous_truncation_operand;
1213                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1214                         $previous_truncation_operand = 1;
1215                     }
1216                     if (scalar @$lefttruncated) {
1217                         $truncated_operand .= "and " if $previous_truncation_operand;
1218                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1219                         $previous_truncation_operand = 1;
1220                     }
1221                     if (scalar @$rightlefttruncated) {
1222                         $truncated_operand .= "and " if $previous_truncation_operand;
1223                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1224                         $previous_truncation_operand = 1;
1225                     }
1226                 }
1227                 $operand = $truncated_operand if $truncated_operand;
1228                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1229
1230                 # Handle Stemming
1231                 my $stemmed_operand;
1232                 $stemmed_operand = _build_stemmed_operand($operand, $lang)
1233                                                                                 if $stemming;
1234
1235                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1236
1237                 # Handle Field Weighting
1238                 my $weighted_operand;
1239                 if ($weight_fields) {
1240                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1241                     $operand = $weighted_operand;
1242                     $indexes_set = 1;
1243                 }
1244
1245                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1246
1247                 # If there's a previous operand, we need to add an operator
1248                 if ($previous_operand) {
1249
1250                     # User-specified operator
1251                     if ( $operators[ $i - 1 ] ) {
1252                         $query     .= " $operators[$i-1] ";
1253                         $query     .= " $index_plus " unless $indexes_set;
1254                         $query     .= " $operand";
1255                         $query_cgi .= "&op=$operators[$i-1]";
1256                         $query_cgi .= "&idx=$index" if $index;
1257                         $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1258                         $query_desc .=
1259                           " $operators[$i-1] $index_plus $operands[$i]";
1260                     }
1261
1262                     # Default operator is and
1263                     else {
1264                         $query      .= " and ";
1265                         $query      .= "$index_plus " unless $indexes_set;
1266                         $query      .= "$operand";
1267                         $query_cgi  .= "&op=and&idx=$index" if $index;
1268                         $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1269                         $query_desc .= " and $index_plus $operands[$i]";
1270                     }
1271                 }
1272
1273                 # There isn't a pervious operand, don't need an operator
1274                 else {
1275
1276                     # Field-weighted queries already have indexes set
1277                     $query .= " $index_plus " unless $indexes_set;
1278                     $query .= $operand;
1279                     $query_desc .= " $index_plus $operands[$i]";
1280                     $query_cgi  .= "&idx=$index" if $index;
1281                     $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1282                     $previous_operand = 1;
1283                 }
1284             }    #/if $operands
1285         }    # /for
1286     }
1287     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1288
1289     # add limits
1290     my $group_OR_limits;
1291     my $availability_limit;
1292     foreach my $this_limit (@limits) {
1293         if ( $this_limit =~ /available/ ) {
1294 #
1295 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1296 ## In English:
1297 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1298             $availability_limit .=
1299 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1300             $limit_cgi  .= "&limit=available";
1301             $limit_desc .= "";
1302         }
1303
1304         # group_OR_limits, prefixed by mc-
1305         # OR every member of the group
1306         elsif ( $this_limit =~ /mc/ ) {
1307         
1308             if ( $this_limit =~ /mc-ccode:/ ) {
1309                 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1310                 $this_limit =~ tr/"//d;
1311                 my ($k,$v) = split(/:/, $this_limit,2);
1312                 $this_limit = $k.":\"".$v."\"";
1313             }
1314
1315             $group_OR_limits .= " or " if $group_OR_limits;
1316             $limit_desc      .= " or " if $group_OR_limits;
1317             $group_OR_limits .= "$this_limit";
1318             $limit_cgi       .= "&limit=$this_limit";
1319             $limit_desc      .= " $this_limit";
1320         }
1321
1322         # Regular old limits
1323         else {
1324             $limit .= " and " if $limit || $query;
1325             $limit      .= "$this_limit";
1326             $limit_cgi  .= "&limit=$this_limit";
1327             if ($this_limit =~ /^branch:(.+)/) {
1328                 my $branchcode = $1;
1329                 my $branchname = GetBranchName($branchcode);
1330                 if (defined $branchname) {
1331                     $limit_desc .= " branch:$branchname";
1332                 } else {
1333                     $limit_desc .= " $this_limit";
1334                 }
1335             } else {
1336                 $limit_desc .= " $this_limit";
1337             }
1338         }
1339     }
1340     if ($group_OR_limits) {
1341         $limit .= " and " if ( $query || $limit );
1342         $limit .= "($group_OR_limits)";
1343     }
1344     if ($availability_limit) {
1345         $limit .= " and " if ( $query || $limit );
1346         $limit .= "($availability_limit)";
1347     }
1348
1349     # Normalize the query and limit strings
1350     # This is flawed , means we can't search anything with : in it
1351     # if user wants to do ccl or cql, start the query with that
1352 #    $query =~ s/:/=/g;
1353     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc)):/=/g;
1354     $query =~ s/(?<=(wrdl)):/=/g;
1355     $query =~ s/(?<=(trn|phr)):/=/g;
1356     $limit =~ s/:/=/g;
1357     for ( $query, $query_desc, $limit, $limit_desc ) {
1358         s/  +/ /g;    # remove extra spaces
1359         s/^ //g;     # remove any beginning spaces
1360         s/ $//g;     # remove any ending spaces
1361         s/==/=/g;    # remove double == from query
1362     }
1363     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1364
1365     for ($query_cgi,$simple_query) {
1366         s/"//g;
1367     }
1368     # append the limit to the query
1369     $query .= " " . $limit;
1370
1371     # Warnings if DEBUG
1372     if ($DEBUG) {
1373         warn "QUERY:" . $query;
1374         warn "QUERY CGI:" . $query_cgi;
1375         warn "QUERY DESC:" . $query_desc;
1376         warn "LIMIT:" . $limit;
1377         warn "LIMIT CGI:" . $limit_cgi;
1378         warn "LIMIT DESC:" . $limit_desc;
1379         warn "---------\nLeave buildQuery\n---------";
1380     }
1381     return (
1382         undef,              $query, $simple_query, $query_cgi,
1383         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1384         $stopwords_removed, $query_type
1385     );
1386 }
1387
1388 =head2 searchResults
1389
1390   my @search_results = searchResults($search_context, $searchdesc, $hits, 
1391                                      $results_per_page, $offset, $scan, 
1392                                      @marcresults, $hidelostitems);
1393
1394 Format results in a form suitable for passing to the template
1395
1396 =cut
1397
1398 # IMO this subroutine is pretty messy still -- it's responsible for
1399 # building the HTML output for the template
1400 sub searchResults {
1401     my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults, $hidelostitems ) = @_;
1402     my $dbh = C4::Context->dbh;
1403     my @newresults;
1404
1405     $search_context = 'opac' unless $search_context eq 'opac' or $search_context eq 'intranet';
1406
1407     #Build branchnames hash
1408     #find branchname
1409     #get branch information.....
1410     my %branches;
1411     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1412     $bsth->execute();
1413     while ( my $bdata = $bsth->fetchrow_hashref ) {
1414         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1415     }
1416 # FIXME - We build an authorised values hash here, using the default framework
1417 # though it is possible to have different authvals for different fws.
1418
1419     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1420
1421     # get notforloan authorised value list (see $shelflocations  FIXME)
1422     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1423
1424     #Build itemtype hash
1425     #find itemtype & itemtype image
1426     my %itemtypes;
1427     $bsth =
1428       $dbh->prepare(
1429         "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1430       );
1431     $bsth->execute();
1432     while ( my $bdata = $bsth->fetchrow_hashref ) {
1433                 foreach (qw(description imageurl summary notforloan)) {
1434                 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1435                 }
1436     }
1437
1438     #search item field code
1439     my $sth =
1440       $dbh->prepare(
1441 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1442       );
1443     $sth->execute;
1444     my ($itemtag) = $sth->fetchrow;
1445
1446     ## find column names of items related to MARC
1447     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1448     $sth2->execute;
1449     my %subfieldstosearch;
1450     while ( ( my $column ) = $sth2->fetchrow ) {
1451         my ( $tagfield, $tagsubfield ) =
1452           &GetMarcFromKohaField( "items." . $column, "" );
1453         $subfieldstosearch{$column} = $tagsubfield;
1454     }
1455
1456     # handle which records to actually retrieve
1457     my $times;
1458     if ( $hits && $offset + $results_per_page <= $hits ) {
1459         $times = $offset + $results_per_page;
1460     }
1461     else {
1462         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1463     }
1464
1465         my $marcflavour = C4::Context->preference("marcflavour");
1466     # We get the biblionumber position in MARC
1467     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1468     my $fw;
1469
1470     # loop through all of the records we've retrieved
1471     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1472         my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
1473         $fw = $scan
1474              ? undef
1475              : $bibliotag < 10
1476                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1477                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1478         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1479         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1480         $oldbiblio->{result_number} = $i + 1;
1481
1482         # add imageurl to itemtype if there is one
1483         $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1484
1485         $oldbiblio->{'authorised_value_images'}  = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
1486                 $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
1487                 $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
1488                 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1489                 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1490                 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1491
1492                 # edition information, if any
1493         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1494                 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1495  # Build summary if there is one (the summary is defined in the itemtypes table)
1496  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1497         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1498             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1499             my @fields  = $marcrecord->fields();
1500
1501             my $newsummary;
1502             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1503                 my $tags = {};
1504                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1505                     $tag =~ /(.{3})(.)/;
1506                     if($marcrecord->field($1)){
1507                         my @abc = $marcrecord->field($1)->subfield($2);
1508                         $tags->{$tag} = $#abc + 1 ;
1509                     }
1510                 }
1511
1512                 # We catch how many times to repeat this line
1513                 my $max = 0;
1514                 foreach my $tag (keys(%$tags)){
1515                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1516                  }
1517
1518                 # we replace, and repeat each line
1519                 for (my $i = 0 ; $i < $max ; $i++){
1520                     my $newline = $line;
1521
1522                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1523                         $tag =~ /(.{3})(.)/;
1524
1525                         if($marcrecord->field($1)){
1526                             my @repl = $marcrecord->field($1)->subfield($2);
1527                             my $subfieldvalue = $repl[$i];
1528
1529                             if (! utf8::is_utf8($subfieldvalue)) {
1530                                 utf8::decode($subfieldvalue);
1531                             }
1532
1533                              $newline =~ s/\[$tag\]/$subfieldvalue/g;
1534                         }
1535                     }
1536                     $newsummary .= "$newline\n";
1537                 }
1538             }
1539
1540             $newsummary =~ s/\[(.*?)]//g;
1541             $newsummary =~ s/\n/<br\/>/g;
1542             $oldbiblio->{summary} = $newsummary;
1543         }
1544
1545         # Pull out the items fields
1546         my @fields = $marcrecord->field($itemtag);
1547
1548         # Setting item statuses for display
1549         my @available_items_loop;
1550         my @onloan_items_loop;
1551         my @other_items_loop;
1552
1553         my $available_items;
1554         my $onloan_items;
1555         my $other_items;
1556
1557         my $ordered_count         = 0;
1558         my $available_count       = 0;
1559         my $onloan_count          = 0;
1560         my $longoverdue_count     = 0;
1561         my $other_count           = 0;
1562         my $wthdrawn_count        = 0;
1563         my $itemlost_count        = 0;
1564         my $itembinding_count     = 0;
1565         my $itemdamaged_count     = 0;
1566         my $item_in_transit_count = 0;
1567         my $can_place_holds       = 0;
1568         my $item_onhold_count     = 0;
1569         my $items_count           = scalar(@fields);
1570         my $maxitems =
1571           ( C4::Context->preference('maxItemsinSearchResults') )
1572           ? C4::Context->preference('maxItemsinSearchResults') - 1
1573           : 1;
1574
1575         # loop through every item
1576         foreach my $field (@fields) {
1577             my $item;
1578
1579             # populate the items hash
1580             foreach my $code ( keys %subfieldstosearch ) {
1581                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1582             }
1583
1584                         my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
1585                         my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1586             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1587             if ($item->{$hbranch}) {
1588                 $item->{'branchname'} = $branches{$item->{$hbranch}};
1589             }
1590             elsif ($item->{$otherbranch}) {     # Last resort
1591                 $item->{'branchname'} = $branches{$item->{$otherbranch}};
1592             }
1593
1594                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1595 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1596             if ( $item->{onloan} ) {
1597                 $onloan_count++;
1598                                 my $key = $prefix . $item->{onloan} . $item->{barcode};
1599                                 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1600                                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1601                                 $onloan_items->{$key}->{branchname} = $item->{branchname};
1602                                 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1603                                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1604                                 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1605                 # if something's checked out and lost, mark it as 'long overdue'
1606                 if ( $item->{itemlost} ) {
1607                     $onloan_items->{$prefix}->{longoverdue}++;
1608                     $longoverdue_count++;
1609                 } else {        # can place holds as long as item isn't lost
1610                     $can_place_holds = 1;
1611                 }
1612             }
1613
1614          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1615             else {
1616
1617                 # item is on order
1618                 if ( $item->{notforloan} == -1 ) {
1619                     $ordered_count++;
1620                 }
1621
1622                 # is item in transit?
1623                 my $transfertwhen = '';
1624                 my ($transfertfrom, $transfertto);
1625
1626                 # is item on the reserve shelf?
1627                 my $reservestatus = 0;
1628                 my $reserveitem;
1629
1630                 unless ($item->{wthdrawn}
1631                         || $item->{itemlost}
1632                         || $item->{damaged}
1633                         || $item->{notforloan}
1634                         || $items_count > 20) {
1635
1636                     # A couple heuristics to limit how many times
1637                     # we query the database for item transfer information, sacrificing
1638                     # accuracy in some cases for speed;
1639                     #
1640                     # 1. don't query if item has one of the other statuses
1641                     # 2. don't check transit status if the bib has
1642                     #    more than 20 items
1643                     #
1644                     # FIXME: to avoid having the query the database like this, and to make
1645                     #        the in transit status count as unavailable for search limiting,
1646                     #        should map transit status to record indexed in Zebra.
1647                     #
1648                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1649                     ($reservestatus, $reserveitem) = C4::Reserves::CheckReserves($item->{itemnumber});
1650                 }
1651
1652                 # item is withdrawn, lost or damaged
1653                 if (   $item->{wthdrawn}
1654                     || $item->{itemlost}
1655                     || $item->{damaged}
1656                     || $item->{notforloan} > 0
1657                     || $reservestatus eq 'Waiting'
1658                     || ($transfertwhen ne ''))
1659                 {
1660                     $wthdrawn_count++        if $item->{wthdrawn};
1661                     $itemlost_count++        if $item->{itemlost};
1662                     $itemdamaged_count++     if $item->{damaged};
1663                     $item_in_transit_count++ if $transfertwhen ne '';
1664                     $item_onhold_count++     if $reservestatus eq 'Waiting';
1665                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1666                     $other_count++;
1667
1668                                         my $key = $prefix . $item->{status};
1669                                         foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
1670                         $other_items->{$key}->{$_} = $item->{$_};
1671                                         }
1672                     $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1673                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
1674                                         $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1675                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
1676                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1677                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1678                 }
1679                 # item is available
1680                 else {
1681                     $can_place_holds = 1;
1682                     $available_count++;
1683                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1684                                         foreach (qw(branchname itemcallnumber)) {
1685                         $available_items->{$prefix}->{$_} = $item->{$_};
1686                                         }
1687                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1688                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1689                 }
1690             }
1691         }    # notforloan, item level and biblioitem level
1692         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
1693         $maxitems =
1694           ( C4::Context->preference('maxItemsinSearchResults') )
1695           ? C4::Context->preference('maxItemsinSearchResults') - 1
1696           : 1;
1697         for my $key ( sort keys %$onloan_items ) {
1698             (++$onloanitemscount > $maxitems) and last;
1699             push @onloan_items_loop, $onloan_items->{$key};
1700         }
1701         for my $key ( sort keys %$other_items ) {
1702             (++$otheritemscount > $maxitems) and last;
1703             push @other_items_loop, $other_items->{$key};
1704         }
1705         for my $key ( sort keys %$available_items ) {
1706             (++$availableitemscount > $maxitems) and last;
1707             push @available_items_loop, $available_items->{$key}
1708         }
1709
1710         # XSLT processing of some stuff
1711         use C4::Charset;
1712         SetUTF8Flag($marcrecord);
1713         $debug && warn $marcrecord->as_formatted;
1714         if (!$scan && $search_context eq 'opac' && C4::Context->preference("OPACXSLTResultsDisplay")) {
1715             # FIXME note that XSLTResultsDisplay (use of XSLT to format staff interface bib search results)
1716             # is not implemented yet
1717             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, 'Results', 
1718                                                                 $search_context, 1);
1719                 # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
1720
1721         }
1722
1723         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
1724         if (!C4::Context->preference("item-level_itypes")) {
1725             if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
1726                 $can_place_holds = 0;
1727             }
1728         }
1729         $oldbiblio->{norequests} = 1 unless $can_place_holds;
1730         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
1731         $oldbiblio->{items_count}          = $items_count;
1732         $oldbiblio->{available_items_loop} = \@available_items_loop;
1733         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
1734         $oldbiblio->{other_items_loop}     = \@other_items_loop;
1735         $oldbiblio->{availablecount}       = $available_count;
1736         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
1737         $oldbiblio->{onloancount}          = $onloan_count;
1738         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
1739         $oldbiblio->{othercount}           = $other_count;
1740         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
1741         $oldbiblio->{wthdrawncount}        = $wthdrawn_count;
1742         $oldbiblio->{itemlostcount}        = $itemlost_count;
1743         $oldbiblio->{damagedcount}         = $itemdamaged_count;
1744         $oldbiblio->{intransitcount}       = $item_in_transit_count;
1745         $oldbiblio->{onholdcount}          = $item_onhold_count;
1746         $oldbiblio->{orderedcount}         = $ordered_count;
1747         $oldbiblio->{isbn} =~
1748           s/-//g;    # deleting - in isbn to enable amazon content
1749         push( @newresults, $oldbiblio )
1750             if(not $hidelostitems
1751                or (($items_count > $itemlost_count )
1752                     && $hidelostitems));
1753     }
1754
1755     return @newresults;
1756 }
1757
1758 =head2 SearchAcquisitions
1759     Search for acquisitions
1760 =cut
1761
1762 sub SearchAcquisitions{
1763     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1764
1765     my $dbh=C4::Context->dbh;
1766     # Variable initialization
1767     my $str=qq|
1768     SELECT marcxml
1769     FROM biblio
1770     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1771     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1772     WHERE dateaccessioned BETWEEN ? AND ?
1773     |;
1774
1775     my (@params,@loopcriteria);
1776
1777     push @params, $datebegin->output("iso");
1778     push @params, $dateend->output("iso");
1779
1780     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1781         if(C4::Context->preference("item-level_itypes")){
1782             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1783         }else{
1784             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1785         }
1786         push @params, @$itemtypes;
1787     }
1788
1789     if ($criteria =~/itemtype/){
1790         if(C4::Context->preference("item-level_itypes")){
1791             $str .= "AND items.itype=? ";
1792         }else{
1793             $str .= "AND biblioitems.itemtype=? ";
1794         }
1795
1796         if(scalar(@$itemtypes) == 0){
1797             my $itypes = GetItemTypes();
1798             for my $key (keys %$itypes){
1799                 push @$itemtypes, $key;
1800             }
1801         }
1802
1803         @loopcriteria= @$itemtypes;
1804     }elsif ($criteria=~/itemcallnumber/){
1805         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
1806                  OR items.itemcallnumber is NULL
1807                  OR items.itemcallnumber = '')";
1808
1809         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
1810     }else {
1811         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1812         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
1813     }
1814
1815     if ($orderby =~ /date_desc/){
1816         $str.=" ORDER BY dateaccessioned DESC";
1817     } else {
1818         $str.=" ORDER BY title";
1819     }
1820
1821     my $qdataacquisitions=$dbh->prepare($str);
1822
1823     my @loopacquisitions;
1824     foreach my $value(@loopcriteria){
1825         push @params,$value;
1826         my %cell;
1827         $cell{"title"}=$value;
1828         $cell{"titlecode"}=$value;
1829
1830         eval{$qdataacquisitions->execute(@params);};
1831
1832         if ($@){ warn "recentacquisitions Error :$@";}
1833         else {
1834             my @loopdata;
1835             while (my $data=$qdataacquisitions->fetchrow_hashref){
1836                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1837             }
1838             $cell{"loopdata"}=\@loopdata;
1839         }
1840         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1841         pop @params;
1842     }
1843     $qdataacquisitions->finish;
1844     return \@loopacquisitions;
1845 }
1846 #----------------------------------------------------------------------
1847 #
1848 # Non-Zebra GetRecords#
1849 #----------------------------------------------------------------------
1850
1851 =head2 NZgetRecords
1852
1853   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1854
1855 =cut
1856
1857 sub NZgetRecords {
1858     my (
1859         $query,            $simple_query, $sort_by_ref,    $servers_ref,
1860         $results_per_page, $offset,       $expanded_facet, $branches,
1861         $query_type,       $scan
1862     ) = @_;
1863     warn "query =$query" if $DEBUG;
1864     my $result = NZanalyse($query);
1865     warn "results =$result" if $DEBUG;
1866     return ( undef,
1867         NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1868         undef );
1869 }
1870
1871 =head2 NZanalyse
1872
1873   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1874   the list is built from an inverted index in the nozebra SQL table
1875   note that title is here only for convenience : the sorting will be very fast when requested on title
1876   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1877
1878 =cut
1879
1880 sub NZanalyse {
1881     my ( $string, $server ) = @_;
1882 #     warn "---------"       if $DEBUG;
1883     warn " NZanalyse" if $DEBUG;
1884 #     warn "---------"       if $DEBUG;
1885
1886  # $server contains biblioserver or authorities, depending on what we search on.
1887  #warn "querying : $string on $server";
1888     $server = 'biblioserver' unless $server;
1889
1890 # if we have a ", replace the content to discard temporarily any and/or/not inside
1891     my $commacontent;
1892     if ( $string =~ /"/ ) {
1893         $string =~ s/"(.*?)"/__X__/;
1894         $commacontent = $1;
1895         warn "commacontent : $commacontent" if $DEBUG;
1896     }
1897
1898 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1899 # then, call again NZanalyse with $left and $right
1900 # (recursive until we find a leaf (=> something without and/or/not)
1901 # delete repeated operator... Would then go in infinite loop
1902     while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1903     }
1904
1905     #process parenthesis before.
1906     if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1907         my $left     = $1;
1908         my $right    = $4;
1909         my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
1910         warn
1911 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1912           if $DEBUG;
1913         my $leftresult = NZanalyse( $left, $server );
1914         if ($operator) {
1915             my $rightresult = NZanalyse( $right, $server );
1916
1917             # OK, we have the results for right and left part of the query
1918             # depending of operand, intersect, union or exclude both lists
1919             # to get a result list
1920             if ( $operator eq ' and ' ) {
1921                 return NZoperatorAND($leftresult,$rightresult);
1922             }
1923             elsif ( $operator eq ' or ' ) {
1924
1925                 # just merge the 2 strings
1926                 return $leftresult . $rightresult;
1927             }
1928             elsif ( $operator eq ' not ' ) {
1929                 return NZoperatorNOT($leftresult,$rightresult);
1930             }
1931         }
1932         else {
1933 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1934             return $leftresult;
1935         }
1936     }
1937     warn "string :" . $string if $DEBUG;
1938     my $left = "";
1939     my $right = "";
1940     my $operator = "";
1941     if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
1942         $left     = $1;
1943         $right    = $3;
1944         $operator = lc($2);    # FIXME: and/or/not are operators, not operands
1945     }
1946     warn "no parenthesis. left : $left operator: $operator right: $right"
1947       if $DEBUG;
1948
1949     # it's not a leaf, we have a and/or/not
1950     if ($operator) {
1951
1952         # reintroduce comma content if needed
1953         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1954         $left  =~ s/__X__/"$commacontent"/ if $commacontent;
1955         warn "node : $left / $operator / $right\n" if $DEBUG;
1956         my $leftresult  = NZanalyse( $left,  $server );
1957         my $rightresult = NZanalyse( $right, $server );
1958         warn " leftresult : $leftresult" if $DEBUG;
1959         warn " rightresult : $rightresult" if $DEBUG;
1960         # OK, we have the results for right and left part of the query
1961         # depending of operand, intersect, union or exclude both lists
1962         # to get a result list
1963         if ( $operator eq ' and ' ) {
1964             return NZoperatorAND($leftresult,$rightresult);
1965         }
1966         elsif ( $operator eq ' or ' ) {
1967
1968             # just merge the 2 strings
1969             return $leftresult . $rightresult;
1970         }
1971         elsif ( $operator eq ' not ' ) {
1972             return NZoperatorNOT($leftresult,$rightresult);
1973         }
1974         else {
1975
1976 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1977             die "error : operand unknown : $operator for $string";
1978         }
1979
1980         # it's a leaf, do the real SQL query and return the result
1981     }
1982     else {
1983         $string =~ s/__X__/"$commacontent"/ if $commacontent;
1984         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1985         #remove trailing blank at the beginning
1986         $string =~ s/^ //g;
1987         warn "leaf:$string" if $DEBUG;
1988
1989         # parse the string in in operator/operand/value again
1990         my $left = "";
1991         my $operator = "";
1992         my $right = "";
1993         if ($string =~ /(.*)(>=|<=)(.*)/) {
1994             $left     = $1;
1995             $operator = $2;
1996             $right    = $3;
1997         } else {
1998             $left = $string;
1999         }
2000 #         warn "handling leaf... left:$left operator:$operator right:$right"
2001 #           if $DEBUG;
2002         unless ($operator) {
2003             if ($string =~ /(.*)(>|<|=)(.*)/) {
2004                 $left     = $1;
2005                 $operator = $2;
2006                 $right    = $3;
2007                 warn
2008     "handling unless (operator)... left:$left operator:$operator right:$right"
2009                 if $DEBUG;
2010             } else {
2011                 $left = $string;
2012             }
2013         }
2014         my $results;
2015
2016 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
2017         $left =~ s/ .*$//;
2018
2019         # automatic replace for short operators
2020         $left = 'title'            if $left =~ '^ti$';
2021         $left = 'author'           if $left =~ '^au$';
2022         $left = 'publisher'        if $left =~ '^pb$';
2023         $left = 'subject'          if $left =~ '^su$';
2024         $left = 'koha-Auth-Number' if $left =~ '^an$';
2025         $left = 'keyword'          if $left =~ '^kw$';
2026         $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
2027         warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
2028         my $dbh = C4::Context->dbh;
2029         if ( $operator && $left ne 'keyword' ) {
2030             #do a specific search
2031             $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
2032             my $sth = $dbh->prepare(
2033 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
2034             );
2035             warn "$left / $operator / $right\n" if $DEBUG;
2036
2037             # split each word, query the DB and build the biblionumbers result
2038             #sanitizing leftpart
2039             $left =~ s/^\s+|\s+$//;
2040             foreach ( split / /, $right ) {
2041                 my $biblionumbers;
2042                 $_ =~ s/^\s+|\s+$//;
2043                 next unless $_;
2044                 warn "EXECUTE : $server, $left, $_" if $DEBUG;
2045                 $sth->execute( $server, $left, $_ )
2046                   or warn "execute failed: $!";
2047                 while ( my ( $line, $value ) = $sth->fetchrow ) {
2048
2049 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
2050 # otherwise, fill the result
2051                     $biblionumbers .= $line
2052                       unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
2053                     warn "result : $value "
2054                       . ( $right  =~ /\d/ ) . "=="
2055                       . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
2056                 }
2057
2058 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2059                 if ($results) {
2060                     warn "NZAND" if $DEBUG;
2061                     $results = NZoperatorAND($biblionumbers,$results);
2062                 } else {
2063                     $results = $biblionumbers;
2064                 }
2065             }
2066         }
2067         else {
2068       #do a complete search (all indexes), if index='kw' do complete search too.
2069             my $sth = $dbh->prepare(
2070 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
2071             );
2072
2073             # split each word, query the DB and build the biblionumbers result
2074             foreach ( split / /, $string ) {
2075                 next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
2076                 warn "search on all indexes on $_" if $DEBUG;
2077                 my $biblionumbers;
2078                 next unless $_;
2079                 $sth->execute( $server, $_ );
2080                 while ( my $line = $sth->fetchrow ) {
2081                     $biblionumbers .= $line;
2082                 }
2083
2084 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2085                 if ($results) {
2086                     $results = NZoperatorAND($biblionumbers,$results);
2087                 }
2088                 else {
2089                     warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
2090                     $results = $biblionumbers;
2091                 }
2092             }
2093         }
2094         warn "return : $results for LEAF : $string" if $DEBUG;
2095         return $results;
2096     }
2097     warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
2098 }
2099
2100 sub NZoperatorAND{
2101     my ($rightresult, $leftresult)=@_;
2102
2103     my @leftresult = split /;/, $leftresult;
2104     warn " @leftresult / $rightresult \n" if $DEBUG;
2105
2106     #             my @rightresult = split /;/,$leftresult;
2107     my $finalresult;
2108
2109 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
2110 # the result is stored twice, to have the same weight for AND than OR.
2111 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
2112 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
2113     foreach (@leftresult) {
2114         my $value = $_;
2115         my $countvalue;
2116         ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
2117         if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
2118             $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
2119             $finalresult .=
2120                 "$value-$countvalue;$value-$countvalue;";
2121         }
2122     }
2123     warn "NZAND DONE : $finalresult \n" if $DEBUG;
2124     return $finalresult;
2125 }
2126
2127 sub NZoperatorOR{
2128     my ($rightresult, $leftresult)=@_;
2129     return $rightresult.$leftresult;
2130 }
2131
2132 sub NZoperatorNOT{
2133     my ($leftresult, $rightresult)=@_;
2134
2135     my @leftresult = split /;/, $leftresult;
2136
2137     #             my @rightresult = split /;/,$leftresult;
2138     my $finalresult;
2139     foreach (@leftresult) {
2140         my $value=$_;
2141         $value=$1 if $value=~m/(.*)-\d+$/;
2142         unless ($rightresult =~ "$value-") {
2143             $finalresult .= "$_;";
2144         }
2145     }
2146     return $finalresult;
2147 }
2148
2149 =head2 NZorder
2150
2151   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
2152
2153   TODO :: Description
2154
2155 =cut
2156
2157 sub NZorder {
2158     my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
2159     warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
2160
2161     # order title asc by default
2162     #     $ordering = '1=36 <i' unless $ordering;
2163     $results_per_page = 20 unless $results_per_page;
2164     $offset           = 0  unless $offset;
2165     my $dbh = C4::Context->dbh;
2166
2167     #
2168     # order by POPULARITY
2169     #
2170     if ( $ordering =~ /popularity/ ) {
2171         my %result;
2172         my %popularity;
2173
2174         # popularity is not in MARC record, it's builded from a specific query
2175         my $sth =
2176           $dbh->prepare("select sum(issues) from items where biblionumber=?");
2177         foreach ( split /;/, $biblionumbers ) {
2178             my ( $biblionumber, $title ) = split /,/, $_;
2179             $result{$biblionumber} = GetMarcBiblio($biblionumber);
2180             $sth->execute($biblionumber);
2181             my $popularity = $sth->fetchrow || 0;
2182
2183 # hint : the key is popularity.title because we can have
2184 # many results with the same popularity. In this case, sub-ordering is done by title
2185 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
2186 # (un-frequent, I agree, but we won't forget anything that way ;-)
2187             $popularity{ sprintf( "%10d", $popularity ) . $title
2188                   . $biblionumber } = $biblionumber;
2189         }
2190
2191     # sort the hash and return the same structure as GetRecords (Zebra querying)
2192         my $result_hash;
2193         my $numbers = 0;
2194         if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
2195             foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
2196                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2197                   $result{ $popularity{$key} }->as_usmarc();
2198             }
2199         }
2200         else {                                    # sort popularity ASC
2201             foreach my $key ( sort ( keys %popularity ) ) {
2202                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2203                   $result{ $popularity{$key} }->as_usmarc();
2204             }
2205         }
2206         my $finalresult = ();
2207         $result_hash->{'hits'}         = $numbers;
2208         $finalresult->{'biblioserver'} = $result_hash;
2209         return $finalresult;
2210
2211         #
2212         # ORDER BY author
2213         #
2214     }
2215     elsif ( $ordering =~ /author/ ) {
2216         my %result;
2217         foreach ( split /;/, $biblionumbers ) {
2218             my ( $biblionumber, $title ) = split /,/, $_;
2219             my $record = GetMarcBiblio($biblionumber);
2220             my $author;
2221             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2222                 $author = $record->subfield( '200', 'f' );
2223                 $author = $record->subfield( '700', 'a' ) unless $author;
2224             }
2225             else {
2226                 $author = $record->subfield( '100', 'a' );
2227             }
2228
2229 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2230 # and we don't want to get only 1 result for each of them !!!
2231             $result{ $author . $biblionumber } = $record;
2232         }
2233
2234     # sort the hash and return the same structure as GetRecords (Zebra querying)
2235         my $result_hash;
2236         my $numbers = 0;
2237         if ( $ordering eq 'author_za' ) {    # sort by author desc
2238             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2239                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2240                   $result{$key}->as_usmarc();
2241             }
2242         }
2243         else {                               # sort by author ASC
2244             foreach my $key ( sort ( keys %result ) ) {
2245                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2246                   $result{$key}->as_usmarc();
2247             }
2248         }
2249         my $finalresult = ();
2250         $result_hash->{'hits'}         = $numbers;
2251         $finalresult->{'biblioserver'} = $result_hash;
2252         return $finalresult;
2253
2254         #
2255         # ORDER BY callnumber
2256         #
2257     }
2258     elsif ( $ordering =~ /callnumber/ ) {
2259         my %result;
2260         foreach ( split /;/, $biblionumbers ) {
2261             my ( $biblionumber, $title ) = split /,/, $_;
2262             my $record = GetMarcBiblio($biblionumber);
2263             my $callnumber;
2264             my $frameworkcode = GetFrameworkCode($biblionumber);
2265             my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField(  'items.itemcallnumber', $frameworkcode);
2266                ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
2267                 unless $callnumber_tag;
2268             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2269                 $callnumber = $record->subfield( '200', 'f' );
2270             } else {
2271                 $callnumber = $record->subfield( '100', 'a' );
2272             }
2273
2274 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2275 # and we don't want to get only 1 result for each of them !!!
2276             $result{ $callnumber . $biblionumber } = $record;
2277         }
2278
2279     # sort the hash and return the same structure as GetRecords (Zebra querying)
2280         my $result_hash;
2281         my $numbers = 0;
2282         if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
2283             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2284                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2285                   $result{$key}->as_usmarc();
2286             }
2287         }
2288         else {                                     # sort by title ASC
2289             foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2290                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2291                   $result{$key}->as_usmarc();
2292             }
2293         }
2294         my $finalresult = ();
2295         $result_hash->{'hits'}         = $numbers;
2296         $finalresult->{'biblioserver'} = $result_hash;
2297         return $finalresult;
2298     }
2299     elsif ( $ordering =~ /pubdate/ ) {             #pub year
2300         my %result;
2301         foreach ( split /;/, $biblionumbers ) {
2302             my ( $biblionumber, $title ) = split /,/, $_;
2303             my $record = GetMarcBiblio($biblionumber);
2304             my ( $publicationyear_tag, $publicationyear_subfield ) =
2305               GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2306             my $publicationyear =
2307               $record->subfield( $publicationyear_tag,
2308                 $publicationyear_subfield );
2309
2310 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2311 # and we don't want to get only 1 result for each of them !!!
2312             $result{ $publicationyear . $biblionumber } = $record;
2313         }
2314
2315     # sort the hash and return the same structure as GetRecords (Zebra querying)
2316         my $result_hash;
2317         my $numbers = 0;
2318         if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
2319             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2320                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2321                   $result{$key}->as_usmarc();
2322             }
2323         }
2324         else {                                 # sort by pub year ASC
2325             foreach my $key ( sort ( keys %result ) ) {
2326                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2327                   $result{$key}->as_usmarc();
2328             }
2329         }
2330         my $finalresult = ();
2331         $result_hash->{'hits'}         = $numbers;
2332         $finalresult->{'biblioserver'} = $result_hash;
2333         return $finalresult;
2334
2335         #
2336         # ORDER BY title
2337         #
2338     }
2339     elsif ( $ordering =~ /title/ ) {
2340
2341 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2342         my %result;
2343         foreach ( split /;/, $biblionumbers ) {
2344             my ( $biblionumber, $title ) = split /,/, $_;
2345
2346 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2347 # and we don't want to get only 1 result for each of them !!!
2348 # hint & speed improvement : we can order without reading the record
2349 # so order, and read records only for the requested page !
2350             $result{ $title . $biblionumber } = $biblionumber;
2351         }
2352
2353     # sort the hash and return the same structure as GetRecords (Zebra querying)
2354         my $result_hash;
2355         my $numbers = 0;
2356         if ( $ordering eq 'title_az' ) {    # sort by title desc
2357             foreach my $key ( sort ( keys %result ) ) {
2358                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2359             }
2360         }
2361         else {                              # sort by title ASC
2362             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2363                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2364             }
2365         }
2366
2367         # limit the $results_per_page to result size if it's more
2368         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2369
2370         # for the requested page, replace biblionumber by the complete record
2371         # speed improvement : avoid reading too much things
2372         for (
2373             my $counter = $offset ;
2374             $counter <= $offset + $results_per_page ;
2375             $counter++
2376           )
2377         {
2378             $result_hash->{'RECORDS'}[$counter] =
2379               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2380         }
2381         my $finalresult = ();
2382         $result_hash->{'hits'}         = $numbers;
2383         $finalresult->{'biblioserver'} = $result_hash;
2384         return $finalresult;
2385     }
2386     else {
2387
2388 #
2389 # order by ranking
2390 #
2391 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2392         my %result;
2393         my %count_ranking;
2394         foreach ( split /;/, $biblionumbers ) {
2395             my ( $biblionumber, $title ) = split /,/, $_;
2396             $title =~ /(.*)-(\d)/;
2397
2398             # get weight
2399             my $ranking = $2;
2400
2401 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2402 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2403 # biblio N has ranking = 6
2404             $count_ranking{$biblionumber} += $ranking;
2405         }
2406
2407 # build the result by "inverting" the count_ranking hash
2408 # 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
2409 #         warn "counting";
2410         foreach ( keys %count_ranking ) {
2411             $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2412         }
2413
2414     # sort the hash and return the same structure as GetRecords (Zebra querying)
2415         my $result_hash;
2416         my $numbers = 0;
2417         foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2418             $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2419         }
2420
2421         # limit the $results_per_page to result size if it's more
2422         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2423
2424         # for the requested page, replace biblionumber by the complete record
2425         # speed improvement : avoid reading too much things
2426         for (
2427             my $counter = $offset ;
2428             $counter <= $offset + $results_per_page ;
2429             $counter++
2430           )
2431         {
2432             $result_hash->{'RECORDS'}[$counter] =
2433               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2434               if $result_hash->{'RECORDS'}[$counter];
2435         }
2436         my $finalresult = ();
2437         $result_hash->{'hits'}         = $numbers;
2438         $finalresult->{'biblioserver'} = $result_hash;
2439         return $finalresult;
2440     }
2441 }
2442
2443 =head2 enabled_staff_search_views
2444
2445 %hash = enabled_staff_search_views()
2446
2447 This function returns a hash that contains three flags obtained from the system
2448 preferences, used to determine whether a particular staff search results view
2449 is enabled.
2450
2451 =over 2
2452
2453 =item C<Output arg:>
2454
2455     * $hash{can_view_MARC} is true only if the MARC view is enabled
2456     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2457     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2458
2459 =item C<usage in the script:>
2460
2461 =back
2462
2463 $template->param ( C4::Search::enabled_staff_search_views );
2464
2465 =cut
2466
2467 sub enabled_staff_search_views
2468 {
2469         return (
2470                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2471                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2472                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2473         );
2474 }
2475
2476 sub AddSearchHistory{
2477         my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2478     my $dbh = C4::Context->dbh;
2479
2480     # Add the request the user just made
2481     my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2482     my $sth   = $dbh->prepare($sql);
2483     $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2484         return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2485 }
2486
2487 sub GetSearchHistory{
2488         my ($borrowernumber,$session)=@_;
2489     my $dbh = C4::Context->dbh;
2490
2491     # Add the request the user just made
2492     my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2493     my $sth   = $dbh->prepare($query);
2494         $sth->execute($borrowernumber, $session);
2495     return  $sth->fetchall_hashref({});
2496 }
2497
2498 =head2 z3950_search_args
2499
2500 $arrayref = z3950_search_args($matchpoints)
2501
2502 This function returns an array reference that contains the search parameters to be
2503 passed to the Z39.50 search script (z3950_search.pl). The array elements
2504 are hash refs whose keys are name, value and encvalue, and whose values are the
2505 name of a search parameter, the value of that search parameter and the URL encoded
2506 value of that parameter.
2507
2508 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2509
2510 The search parameter values are obtained from the bibliographic record whose
2511 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2512
2513 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2514 a general purpose search argument. In this case, the returned array contains only
2515 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2516
2517 If a search parameter value is undefined or empty, it is not included in the returned
2518 array.
2519
2520 The returned array reference may be passed directly to the template parameters.
2521
2522 =over 2
2523
2524 =item C<Output arg:>
2525
2526     * $array containing hash refs as described above
2527
2528 =item C<usage in the script:>
2529
2530 =back
2531
2532 $data = Biblio::GetBiblioData($bibno);
2533 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2534
2535 *OR*
2536
2537 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2538
2539 =cut
2540
2541 sub z3950_search_args {
2542     my $bibrec = shift;
2543     $bibrec = { title => $bibrec } if !ref $bibrec;
2544     my $array = [];
2545     for my $field (qw/ lccn isbn issn title author dewey subject /)
2546     {
2547         my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2548         push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2549     }
2550     return $array;
2551 }
2552
2553 =head2 BiblioAddAuthorities
2554
2555 ( $countlinked, $countcreated ) = BiblioAddAuthorities($record, $frameworkcode);
2556
2557 this function finds the authorities linked to the biblio
2558     * search in the authority DB for the same authid (in $9 of the biblio)
2559     * search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
2560     * search in the authority DB for the same values (exactly) (in all subfields of the biblio)
2561 OR adds a new authority record
2562
2563 =over 2
2564
2565 =item C<input arg:>
2566
2567     * $record is the MARC record in question (marc blob)
2568     * $frameworkcode is the bibliographic framework to use (if it is "" it uses the default framework)
2569
2570 =item C<Output arg:>
2571
2572     * $countlinked is the number of authorities records that are linked to this authority
2573     * $countcreated
2574
2575 =item C<BUGS>
2576     * I had to add this to Search.pm (instead of the logical Biblio.pm) because of a circular dependency (this sub uses SimpleSearch, and Search.pm uses Biblio.pm)
2577
2578 =back
2579
2580 =cut
2581
2582
2583 sub BiblioAddAuthorities{
2584   my ( $record, $frameworkcode ) = @_;
2585   my $dbh=C4::Context->dbh;
2586   my $query=$dbh->prepare(qq|
2587 SELECT authtypecode,tagfield
2588 FROM marc_subfield_structure
2589 WHERE frameworkcode=?
2590 AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
2591 # SELECT authtypecode,tagfield
2592 # FROM marc_subfield_structure
2593 # WHERE frameworkcode=?
2594 # AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
2595   $query->execute($frameworkcode);
2596   my ($countcreated,$countlinked);
2597   while (my $data=$query->fetchrow_hashref){
2598     foreach my $field ($record->field($data->{tagfield})){
2599       next if ($field->subfield('3')||$field->subfield('9'));
2600       # No authorities id in the tag.
2601       # Search if there is any authorities to link to.
2602       my $query='at='.$data->{authtypecode}.' ';
2603       map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)}  $field->subfields();
2604       my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
2605     # there is only 1 result
2606           if ( $error ) {
2607         warn "BIBLIOADDSAUTHORITIES: $error";
2608             return (0,0) ;
2609           }
2610       if ($results && scalar(@$results)==1) {
2611         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2612         $field->add_subfields('9'=>$marcrecord->field('001')->data);
2613         $countlinked++;
2614       } elsif (scalar(@$results)>1) {
2615    #More than One result
2616    #This can comes out of a lack of a subfield.
2617 #         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2618 #         $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
2619   $countlinked++;
2620       } else {
2621   #There are no results, build authority record, add it to Authorities, get authid and add it to 9
2622   ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode
2623   ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
2624          my $authtypedata=C4::AuthoritiesMarc::GetAuthType($data->{authtypecode});
2625          next unless $authtypedata;
2626          my $marcrecordauth=MARC::Record->new();
2627          my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
2628          map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )}  $field->subfields();
2629          $marcrecordauth->insert_fields_ordered($authfield);
2630
2631          # bug 2317: ensure new authority knows it's using UTF-8; currently
2632          # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
2633          # automatically for UNIMARC (by not transcoding)
2634          # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
2635          # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
2636          # of change to a core API just before the 3.0 release.
2637          if (C4::Context->preference('marcflavour') eq 'MARC21') {
2638             SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
2639          }
2640
2641 #          warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
2642
2643          my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
2644          $countcreated++;
2645          $field->add_subfields('9'=>$authid);
2646       }
2647     }
2648   }
2649   return ($countlinked,$countcreated);
2650 }
2651
2652 =head2 GetDistinctValues($field);
2653
2654 C<$field> is a reference to the fields array
2655
2656 =cut
2657
2658 sub GetDistinctValues {
2659     my ($fieldname,$string)=@_;
2660     # returns a reference to a hash of references to branches...
2661     if ($fieldname=~/\./){
2662                         my ($table,$column)=split /\./, $fieldname;
2663                         my $dbh = C4::Context->dbh;
2664                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2665                         my $sth = $dbh->prepare("select DISTINCT($column) as value, count(*) as cnt from $table ".($string?" where $column like \"$string%\"":"")."group by value order by $column ");
2666                         $sth->execute;
2667                         my $elements=$sth->fetchall_arrayref({});
2668                         return $elements;
2669    }
2670    else {
2671                 $string||= qq("");
2672                 my @servers=qw<biblioserver authorityserver>;
2673                 my (@zconns,@results);
2674         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2675                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2676                         $results[$i] =
2677                       $zconns[$i]->scan(
2678                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2679                       );
2680                 }
2681                 # The big moment: asynchronously retrieve results from all servers
2682                 my @elements;
2683                 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
2684                         my $ev = $zconns[ $i - 1 ]->last_event();
2685                         if ( $ev == ZOOM::Event::ZEND ) {
2686                                 next unless $results[ $i - 1 ];
2687                                 my $size = $results[ $i - 1 ]->size();
2688                                 if ( $size > 0 ) {
2689                       for (my $j=0;$j<$size;$j++){
2690                                                 my %hashscan;
2691                                                 @hashscan{qw(value cnt)}=$results[ $i - 1 ]->display_term($j);
2692                                                 push @elements, \%hashscan;
2693                                           }
2694                                 }
2695                         }
2696                 }
2697                 return \@elements;
2698    }
2699 }
2700
2701
2702 END { }    # module clean-up code here (global destructor)
2703
2704 1;
2705 __END__
2706
2707 =head1 AUTHOR
2708
2709 Koha Development Team <http://koha-community.org/>
2710
2711 =cut