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