Bug 12788: (followup) minor optimization with proper tests
[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                 ($query,$query_cgi,$query_desc,$previous_operand) = _build_initial_query({
1536                     query => $query,
1537                     query_cgi => $query_cgi,
1538                     query_desc => $query_desc,
1539                     operator => ($operators[ $i - 1 ]) ? $operators[ $i - 1 ] : '',
1540                     parsed_operand => $operand,
1541                     original_operand => ($operands[$i]) ? $operands[$i] : '',
1542                     index => $index,
1543                     index_plus => $index_plus,
1544                     indexes_set => $indexes_set,
1545                     previous_operand => $previous_operand,
1546                 });
1547
1548             }    #/if $operands
1549         }    # /for
1550     }
1551     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1552
1553     # add limits
1554     my %group_OR_limits;
1555     my $availability_limit;
1556     foreach my $this_limit (@limits) {
1557         next unless $this_limit;
1558         if ( $this_limit =~ /available/ ) {
1559 #
1560 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1561 ## In English:
1562 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1563             $availability_limit .=
1564 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1565             $limit_cgi  .= "&limit=available";
1566             $limit_desc .= "";
1567         }
1568
1569         # group_OR_limits, prefixed by mc-
1570         # OR every member of the group
1571         elsif ( $this_limit =~ /mc/ ) {
1572             my ($k,$v) = split(/:/, $this_limit,2);
1573             if ( $k !~ /mc-i(tem)?type/ ) {
1574                 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1575                 $this_limit =~ tr/"//d;
1576                 $this_limit = $k.":\"".$v."\"";
1577             }
1578
1579             $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
1580             $limit_desc      .= " or " if $group_OR_limits{$k};
1581             $group_OR_limits{$k} .= "$this_limit";
1582             $limit_cgi       .= "&limit=" . uri_escape($this_limit);
1583             $limit_desc      .= " $this_limit";
1584         }
1585
1586         # Regular old limits
1587         else {
1588             $limit .= " and " if $limit || $query;
1589             $limit      .= "$this_limit";
1590             $limit_cgi  .= "&limit=" . uri_escape($this_limit);
1591             if ($this_limit =~ /^branch:(.+)/) {
1592                 my $branchcode = $1;
1593                 my $branchname = GetBranchName($branchcode);
1594                 if (defined $branchname) {
1595                     $limit_desc .= " branch:$branchname";
1596                 } else {
1597                     $limit_desc .= " $this_limit";
1598                 }
1599             } else {
1600                 $limit_desc .= " $this_limit";
1601             }
1602         }
1603     }
1604     foreach my $k (keys (%group_OR_limits)) {
1605         $limit .= " and " if ( $query || $limit );
1606         $limit .= "($group_OR_limits{$k})";
1607     }
1608     if ($availability_limit) {
1609         $limit .= " and " if ( $query || $limit );
1610         $limit .= "($availability_limit)";
1611     }
1612
1613     # Normalize the query and limit strings
1614     # This is flawed , means we can't search anything with : in it
1615     # if user wants to do ccl or cql, start the query with that
1616 #    $query =~ s/:/=/g;
1617     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
1618     $query =~ s/(?<=(wrdl)):/=/g;
1619     $query =~ s/(?<=(trn|phr)):/=/g;
1620     $limit =~ s/:/=/g;
1621     for ( $query, $query_desc, $limit, $limit_desc ) {
1622         s/  +/ /g;    # remove extra spaces
1623         s/^ //g;     # remove any beginning spaces
1624         s/ $//g;     # remove any ending spaces
1625         s/==/=/g;    # remove double == from query
1626     }
1627     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1628
1629     for ($query_cgi,$simple_query) {
1630         s/"//g;
1631     }
1632     # append the limit to the query
1633     $query .= " " . $limit;
1634
1635     # Warnings if DEBUG
1636     if ($DEBUG) {
1637         warn "QUERY:" . $query;
1638         warn "QUERY CGI:" . $query_cgi;
1639         warn "QUERY DESC:" . $query_desc;
1640         warn "LIMIT:" . $limit;
1641         warn "LIMIT CGI:" . $limit_cgi;
1642         warn "LIMIT DESC:" . $limit_desc;
1643         warn "---------\nLeave buildQuery\n---------";
1644     }
1645     return (
1646         undef,              $query, $simple_query, $query_cgi,
1647         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1648         $stopwords_removed, $query_type
1649     );
1650 }
1651
1652 =head2 _build_initial_query
1653
1654   ($query, $query_cgi, $query_desc, $previous_operand) = _build_initial_query($initial_query_params);
1655
1656   Build a section of the initial query containing indexes, operators, and operands.
1657
1658 =cut
1659
1660 sub _build_initial_query {
1661     my ($params) = @_;
1662
1663     my $operator = "";
1664     if ($params->{previous_operand}){
1665         #If there is a previous operand, add a supplied operator or the default 'and'
1666         $operator = ($params->{operator}) ? " ".($params->{operator})." " : ' and ';
1667     }
1668
1669     #NOTE: indexes_set is typically set when doing truncation or field weighting
1670     my $operand = ($params->{indexes_set}) ? $params->{parsed_operand} : $params->{index_plus}.$params->{parsed_operand};
1671
1672     #e.g. "kw,wrdl:test"
1673     #e.g. " and kw,wrdl:test"
1674     $params->{query} .= $operator . $operand;
1675
1676     $params->{query_cgi} .= "&op=".uri_escape($operator) if $operator;
1677     $params->{query_cgi} .= "&idx=".uri_escape($params->{index}) if $params->{index};
1678     $params->{query_cgi} .= "&q=".uri_escape($params->{original_operand}) if $params->{original_operand};
1679
1680     #e.g. " and kw,wrdl: test"
1681     $params->{query_desc} .= $operator . $params->{index_plus} . " " . $params->{original_operand};
1682
1683     $params->{previous_operand} = 1 unless $params->{previous_operand}; #If there is no previous operand, mark this as one
1684
1685     return ($params->{query}, $params->{query_cgi}, $params->{query_desc}, $params->{previous_operand});
1686 }
1687
1688 =head2 searchResults
1689
1690   my @search_results = searchResults($search_context, $searchdesc, $hits, 
1691                                      $results_per_page, $offset, $scan, 
1692                                      @marcresults);
1693
1694 Format results in a form suitable for passing to the template
1695
1696 =cut
1697
1698 # IMO this subroutine is pretty messy still -- it's responsible for
1699 # building the HTML output for the template
1700 sub searchResults {
1701     my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
1702     my $dbh = C4::Context->dbh;
1703     my @newresults;
1704
1705     require C4::Items;
1706
1707     $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
1708     my ($is_opac, $hidelostitems);
1709     if ($search_context eq 'opac') {
1710         $hidelostitems = C4::Context->preference('hidelostitems');
1711         $is_opac       = 1;
1712     }
1713
1714     #Build branchnames hash
1715     #find branchname
1716     #get branch information.....
1717     my %branches;
1718     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1719     $bsth->execute();
1720     while ( my $bdata = $bsth->fetchrow_hashref ) {
1721         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1722     }
1723 # FIXME - We build an authorised values hash here, using the default framework
1724 # though it is possible to have different authvals for different fws.
1725
1726     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1727
1728     # get notforloan authorised value list (see $shelflocations  FIXME)
1729     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1730
1731     #Build itemtype hash
1732     #find itemtype & itemtype image
1733     my %itemtypes;
1734     $bsth =
1735       $dbh->prepare(
1736         "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1737       );
1738     $bsth->execute();
1739     while ( my $bdata = $bsth->fetchrow_hashref ) {
1740                 foreach (qw(description imageurl summary notforloan)) {
1741                 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1742                 }
1743     }
1744
1745     #search item field code
1746     my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber", "" );
1747
1748     ## find column names of items related to MARC
1749     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1750     $sth2->execute;
1751     my %subfieldstosearch;
1752     while ( ( my $column ) = $sth2->fetchrow ) {
1753         my ( $tagfield, $tagsubfield ) =
1754           &GetMarcFromKohaField( "items." . $column, "" );
1755         if ( defined $tagsubfield ) {
1756             $subfieldstosearch{$column} = $tagsubfield;
1757         }
1758     }
1759
1760     # handle which records to actually retrieve
1761     my $times;
1762     if ( $hits && $offset + $results_per_page <= $hits ) {
1763         $times = $offset + $results_per_page;
1764     }
1765     else {
1766         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1767     }
1768
1769     my $marcflavour = C4::Context->preference("marcflavour");
1770     # We get the biblionumber position in MARC
1771     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1772
1773     # loop through all of the records we've retrieved
1774     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1775
1776         my $marcrecord;
1777         if ($scan) {
1778             # For Scan searches we built USMARC data
1779             $marcrecord = MARC::Record->new_from_usmarc( $marcresults->[$i]);
1780         } else {
1781             # Normal search, render from Zebra's output
1782             $marcrecord = new_record_from_zebra(
1783                 'biblioserver',
1784                 $marcresults->[$i]
1785             );
1786
1787             if ( ! defined $marcrecord ) {
1788                 warn "ERROR DECODING RECORD - $@: " . $marcresults->[$i];
1789                 next;
1790             }
1791         }
1792
1793         my $fw = $scan
1794              ? undef
1795              : $bibliotag < 10
1796                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1797                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1798         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1799         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1800         $oldbiblio->{result_number} = $i + 1;
1801
1802         # add imageurl to itemtype if there is one
1803         $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1804
1805         $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 ) ) : [];
1806                 $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
1807                 $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
1808                 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1809                 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1810                 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1811
1812                 # edition information, if any
1813         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1814                 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1815  # Build summary if there is one (the summary is defined in the itemtypes table)
1816  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1817         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1818             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1819             my @fields  = $marcrecord->fields();
1820
1821             my $newsummary;
1822             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1823                 my $tags = {};
1824                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1825                     $tag =~ /(.{3})(.)/;
1826                     if($marcrecord->field($1)){
1827                         my @abc = $marcrecord->field($1)->subfield($2);
1828                         $tags->{$tag} = $#abc + 1 ;
1829                     }
1830                 }
1831
1832                 # We catch how many times to repeat this line
1833                 my $max = 0;
1834                 foreach my $tag (keys(%$tags)){
1835                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1836                  }
1837
1838                 # we replace, and repeat each line
1839                 for (my $i = 0 ; $i < $max ; $i++){
1840                     my $newline = $line;
1841
1842                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1843                         $tag =~ /(.{3})(.)/;
1844
1845                         if($marcrecord->field($1)){
1846                             my @repl = $marcrecord->field($1)->subfield($2);
1847                             my $subfieldvalue = $repl[$i];
1848
1849                             if (! utf8::is_utf8($subfieldvalue)) {
1850                                 utf8::decode($subfieldvalue);
1851                             }
1852
1853                              $newline =~ s/\[$tag\]/$subfieldvalue/g;
1854                         }
1855                     }
1856                     $newsummary .= "$newline\n";
1857                 }
1858             }
1859
1860             $newsummary =~ s/\[(.*?)]//g;
1861             $newsummary =~ s/\n/<br\/>/g;
1862             $oldbiblio->{summary} = $newsummary;
1863         }
1864
1865         # Pull out the items fields
1866         my @fields = $marcrecord->field($itemtag);
1867         my $marcflavor = C4::Context->preference("marcflavour");
1868         # adding linked items that belong to host records
1869         my $analyticsfield = '773';
1870         if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
1871             $analyticsfield = '773';
1872         } elsif ($marcflavor eq 'UNIMARC') {
1873             $analyticsfield = '461';
1874         }
1875         foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
1876             my $hostbiblionumber = $hostfield->subfield("0");
1877             my $linkeditemnumber = $hostfield->subfield("9");
1878             if(!$hostbiblionumber eq undef){
1879                 my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
1880                 my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
1881                 if(!$hostbiblio eq undef){
1882                     my @hostitems = $hostbiblio->field($itemfield);
1883                     foreach my $hostitem (@hostitems){
1884                         if ($hostitem->subfield("9") eq $linkeditemnumber){
1885                             my $linkeditem =$hostitem;
1886                             # append linked items if they exist
1887                             if (!$linkeditem eq undef){
1888                                 push (@fields, $linkeditem);}
1889                         }
1890                     }
1891                 }
1892             }
1893         }
1894
1895         # Setting item statuses for display
1896         my @available_items_loop;
1897         my @onloan_items_loop;
1898         my @other_items_loop;
1899
1900         my $available_items;
1901         my $onloan_items;
1902         my $other_items;
1903
1904         my $ordered_count         = 0;
1905         my $available_count       = 0;
1906         my $onloan_count          = 0;
1907         my $longoverdue_count     = 0;
1908         my $other_count           = 0;
1909         my $withdrawn_count        = 0;
1910         my $itemlost_count        = 0;
1911         my $hideatopac_count      = 0;
1912         my $itembinding_count     = 0;
1913         my $itemdamaged_count     = 0;
1914         my $item_in_transit_count = 0;
1915         my $can_place_holds       = 0;
1916         my $item_onhold_count     = 0;
1917         my $notforloan_count      = 0;
1918         my $items_count           = scalar(@fields);
1919         my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
1920         my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
1921         my @hiddenitems; # hidden itemnumbers based on OpacHiddenItems syspref
1922
1923         # loop through every item
1924         foreach my $field (@fields) {
1925             my $item;
1926
1927             # populate the items hash
1928             foreach my $code ( keys %subfieldstosearch ) {
1929                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1930             }
1931             $item->{description} = $itemtypes{ $item->{itype} }{description};
1932
1933                 # OPAC hidden items
1934             if ($is_opac) {
1935                 # hidden because lost
1936                 if ($hidelostitems && $item->{itemlost}) {
1937                     $hideatopac_count++;
1938                     next;
1939                 }
1940                 # hidden based on OpacHiddenItems syspref
1941                 my @hi = C4::Items::GetHiddenItemnumbers($item);
1942                 if (scalar @hi) {
1943                     push @hiddenitems, @hi;
1944                     $hideatopac_count++;
1945                     next;
1946                 }
1947             }
1948
1949             my $hbranch     = C4::Context->preference('StaffSearchResultsDisplayBranch');
1950             my $otherbranch = $hbranch eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1951
1952             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1953             if ($item->{$hbranch}) {
1954                 $item->{'branchname'} = $branches{$item->{$hbranch}};
1955             }
1956             elsif ($item->{$otherbranch}) {     # Last resort
1957                 $item->{'branchname'} = $branches{$item->{$otherbranch}};
1958             }
1959
1960                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1961 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1962             my $userenv = C4::Context->userenv;
1963             if ( $item->{onloan} && !(C4::Members::GetHideLostItemsPreference($userenv->{'number'}) && $item->{itemlost}) ) {
1964                 $onloan_count++;
1965                                 my $key = $prefix . $item->{onloan} . $item->{barcode};
1966                                 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1967                                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1968                                 $onloan_items->{$key}->{branchname} = $item->{branchname};
1969                                 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1970                                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1971                                 $onloan_items->{$key}->{description} = $item->{description};
1972                                 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1973                 # if something's checked out and lost, mark it as 'long overdue'
1974                 if ( $item->{itemlost} ) {
1975                     $onloan_items->{$prefix}->{longoverdue}++;
1976                     $longoverdue_count++;
1977                 } else {        # can place holds as long as item isn't lost
1978                     $can_place_holds = 1;
1979                 }
1980             }
1981
1982          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1983             else {
1984
1985                 # item is on order
1986                 if ( $item->{notforloan} < 0 ) {
1987                     $ordered_count++;
1988                 } elsif ( $item->{notforloan} > 0 ) {
1989                     $notforloan_count++;
1990                 }
1991
1992                 # is item in transit?
1993                 my $transfertwhen = '';
1994                 my ($transfertfrom, $transfertto);
1995
1996                 # is item on the reserve shelf?
1997                 my $reservestatus = '';
1998
1999                 unless ($item->{withdrawn}
2000                         || $item->{itemlost}
2001                         || $item->{damaged}
2002                         || $item->{notforloan}
2003                         || $items_count > 20) {
2004
2005                     # A couple heuristics to limit how many times
2006                     # we query the database for item transfer information, sacrificing
2007                     # accuracy in some cases for speed;
2008                     #
2009                     # 1. don't query if item has one of the other statuses
2010                     # 2. don't check transit status if the bib has
2011                     #    more than 20 items
2012                     #
2013                     # FIXME: to avoid having the query the database like this, and to make
2014                     #        the in transit status count as unavailable for search limiting,
2015                     #        should map transit status to record indexed in Zebra.
2016                     #
2017                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
2018                     $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber}, $oldbiblio->{biblionumber} );
2019                 }
2020
2021                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
2022                 if (   $item->{withdrawn}
2023                     || $item->{itemlost}
2024                     || $item->{damaged}
2025                     || $item->{notforloan}
2026                     || $reservestatus eq 'Waiting'
2027                     || ($transfertwhen ne ''))
2028                 {
2029                     $withdrawn_count++        if $item->{withdrawn};
2030                     $itemlost_count++        if $item->{itemlost};
2031                     $itemdamaged_count++     if $item->{damaged};
2032                     $item_in_transit_count++ if $transfertwhen ne '';
2033                     $item_onhold_count++     if $reservestatus eq 'Waiting';
2034                     $item->{status} = $item->{withdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
2035
2036                     # can place a hold on a item if
2037                     # not lost nor withdrawn
2038                     # not damaged unless AllowHoldsOnDamagedItems is true
2039                     # item is either for loan or on order (notforloan < 0)
2040                     $can_place_holds = 1
2041                       if (
2042                            !$item->{itemlost}
2043                         && !$item->{withdrawn}
2044                         && ( !$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems') )
2045                         && ( !$item->{notforloan} || $item->{notforloan} < 0 )
2046                       );
2047
2048                     $other_count++;
2049
2050                     my $key = $prefix . $item->{status};
2051                     foreach (qw(withdrawn itemlost damaged branchname itemcallnumber)) {
2052                         $other_items->{$key}->{$_} = $item->{$_};
2053                     }
2054                     $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
2055                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
2056                     $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
2057                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
2058                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
2059                                         $other_items->{$key}->{description} = $item->{description};
2060                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2061                 }
2062                 # item is available
2063                 else {
2064                     $can_place_holds = 1;
2065                     $available_count++;
2066                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
2067                                         foreach (qw(branchname itemcallnumber description)) {
2068                         $available_items->{$prefix}->{$_} = $item->{$_};
2069                                         }
2070                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
2071                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2072                 }
2073             }
2074         }    # notforloan, item level and biblioitem level
2075
2076         # if all items are hidden, do not show the record
2077         if ($items_count > 0 && $hideatopac_count == $items_count) {
2078             next;
2079         }
2080
2081         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
2082         for my $key ( sort keys %$onloan_items ) {
2083             (++$onloanitemscount > $maxitems) and last;
2084             push @onloan_items_loop, $onloan_items->{$key};
2085         }
2086         for my $key ( sort keys %$other_items ) {
2087             (++$otheritemscount > $maxitems) and last;
2088             push @other_items_loop, $other_items->{$key};
2089         }
2090         for my $key ( sort keys %$available_items ) {
2091             (++$availableitemscount > $maxitems) and last;
2092             push @available_items_loop, $available_items->{$key}
2093         }
2094
2095         # XSLT processing of some stuff
2096         SetUTF8Flag($marcrecord);
2097         warn $marcrecord->as_formatted if $DEBUG;
2098         my $interface = $search_context eq 'opac' ? 'OPAC' : '';
2099         if (!$scan && C4::Context->preference($interface . "XSLTResultsDisplay")) {
2100             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $interface."XSLTResultsDisplay", 1, \@hiddenitems);
2101         # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
2102         }
2103
2104         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
2105         if (!C4::Context->preference("item-level_itypes")) {
2106             if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
2107                 $can_place_holds = 0;
2108             }
2109         }
2110         $oldbiblio->{norequests} = 1 unless $can_place_holds;
2111         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
2112         $oldbiblio->{items_count}          = $items_count;
2113         $oldbiblio->{available_items_loop} = \@available_items_loop;
2114         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
2115         $oldbiblio->{other_items_loop}     = \@other_items_loop;
2116         $oldbiblio->{availablecount}       = $available_count;
2117         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
2118         $oldbiblio->{onloancount}          = $onloan_count;
2119         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
2120         $oldbiblio->{othercount}           = $other_count;
2121         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
2122         $oldbiblio->{withdrawncount}        = $withdrawn_count;
2123         $oldbiblio->{itemlostcount}        = $itemlost_count;
2124         $oldbiblio->{damagedcount}         = $itemdamaged_count;
2125         $oldbiblio->{intransitcount}       = $item_in_transit_count;
2126         $oldbiblio->{onholdcount}          = $item_onhold_count;
2127         $oldbiblio->{orderedcount}         = $ordered_count;
2128         $oldbiblio->{notforloancount}      = $notforloan_count;
2129
2130         if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
2131             my $fieldspec = C4::Context->preference("AlternateHoldingsField");
2132             my $subfields = substr $fieldspec, 3;
2133             my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
2134             my @alternateholdingsinfo = ();
2135             my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
2136             my $alternateholdingscount = 0;
2137
2138             for my $field (@holdingsfields) {
2139                 my %holding = ( holding => '' );
2140                 my $havesubfield = 0;
2141                 for my $subfield ($field->subfields()) {
2142                     if ((index $subfields, $$subfield[0]) >= 0) {
2143                         $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
2144                         $holding{'holding'} .= $$subfield[1];
2145                         $havesubfield++;
2146                     }
2147                 }
2148                 if ($havesubfield) {
2149                     push(@alternateholdingsinfo, \%holding);
2150                     $alternateholdingscount++;
2151                 }
2152             }
2153
2154             $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
2155             $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
2156         }
2157
2158         push( @newresults, $oldbiblio );
2159     }
2160
2161     return @newresults;
2162 }
2163
2164 =head2 SearchAcquisitions
2165     Search for acquisitions
2166 =cut
2167
2168 sub SearchAcquisitions{
2169     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
2170
2171     my $dbh=C4::Context->dbh;
2172     # Variable initialization
2173     my $str=qq|
2174     SELECT marcxml
2175     FROM biblio
2176     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
2177     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
2178     WHERE dateaccessioned BETWEEN ? AND ?
2179     |;
2180
2181     my (@params,@loopcriteria);
2182
2183     push @params, $datebegin->output("iso");
2184     push @params, $dateend->output("iso");
2185
2186     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
2187         if(C4::Context->preference("item-level_itypes")){
2188             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
2189         }else{
2190             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
2191         }
2192         push @params, @$itemtypes;
2193     }
2194
2195     if ($criteria =~/itemtype/){
2196         if(C4::Context->preference("item-level_itypes")){
2197             $str .= "AND items.itype=? ";
2198         }else{
2199             $str .= "AND biblioitems.itemtype=? ";
2200         }
2201
2202         if(scalar(@$itemtypes) == 0){
2203             my $itypes = GetItemTypes();
2204             for my $key (keys %$itypes){
2205                 push @$itemtypes, $key;
2206             }
2207         }
2208
2209         @loopcriteria= @$itemtypes;
2210     }elsif ($criteria=~/itemcallnumber/){
2211         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
2212                  OR items.itemcallnumber is NULL
2213                  OR items.itemcallnumber = '')";
2214
2215         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
2216     }else {
2217         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
2218         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
2219     }
2220
2221     if ($orderby =~ /date_desc/){
2222         $str.=" ORDER BY dateaccessioned DESC";
2223     } else {
2224         $str.=" ORDER BY title";
2225     }
2226
2227     my $qdataacquisitions=$dbh->prepare($str);
2228
2229     my @loopacquisitions;
2230     foreach my $value(@loopcriteria){
2231         push @params,$value;
2232         my %cell;
2233         $cell{"title"}=$value;
2234         $cell{"titlecode"}=$value;
2235
2236         eval{$qdataacquisitions->execute(@params);};
2237
2238         if ($@){ warn "recentacquisitions Error :$@";}
2239         else {
2240             my @loopdata;
2241             while (my $data=$qdataacquisitions->fetchrow_hashref){
2242                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
2243             }
2244             $cell{"loopdata"}=\@loopdata;
2245         }
2246         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
2247         pop @params;
2248     }
2249     $qdataacquisitions->finish;
2250     return \@loopacquisitions;
2251 }
2252
2253 =head2 enabled_staff_search_views
2254
2255 %hash = enabled_staff_search_views()
2256
2257 This function returns a hash that contains three flags obtained from the system
2258 preferences, used to determine whether a particular staff search results view
2259 is enabled.
2260
2261 =over 2
2262
2263 =item C<Output arg:>
2264
2265     * $hash{can_view_MARC} is true only if the MARC view is enabled
2266     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2267     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2268
2269 =item C<usage in the script:>
2270
2271 =back
2272
2273 $template->param ( C4::Search::enabled_staff_search_views );
2274
2275 =cut
2276
2277 sub enabled_staff_search_views
2278 {
2279         return (
2280                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2281                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2282                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2283         );
2284 }
2285
2286 sub PurgeSearchHistory{
2287     my ($pSearchhistory)=@_;
2288     my $dbh = C4::Context->dbh;
2289     my $sth = $dbh->prepare("DELETE FROM search_history WHERE time < DATE_SUB( NOW(), INTERVAL ? DAY )");
2290     $sth->execute($pSearchhistory) or die $dbh->errstr;
2291 }
2292
2293 =head2 z3950_search_args
2294
2295 $arrayref = z3950_search_args($matchpoints)
2296
2297 This function returns an array reference that contains the search parameters to be
2298 passed to the Z39.50 search script (z3950_search.pl). The array elements
2299 are hash refs whose keys are name and value, and whose values are the
2300 name of a search parameter, the value of that search parameter and the URL encoded
2301 value of that parameter.
2302
2303 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2304
2305 The search parameter values are obtained from the bibliographic record whose
2306 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2307
2308 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2309 a general purpose search argument. In this case, the returned array contains only
2310 entry: the key is 'title' and the value is derived from $matchpoints.
2311
2312 If a search parameter value is undefined or empty, it is not included in the returned
2313 array.
2314
2315 The returned array reference may be passed directly to the template parameters.
2316
2317 =over 2
2318
2319 =item C<Output arg:>
2320
2321     * $array containing hash refs as described above
2322
2323 =item C<usage in the script:>
2324
2325 =back
2326
2327 $data = Biblio::GetBiblioData($bibno);
2328 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2329
2330 *OR*
2331
2332 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2333
2334 =cut
2335
2336 sub z3950_search_args {
2337     my $bibrec = shift;
2338
2339     my $isbn_string = ref( $bibrec ) ? $bibrec->{title} : $bibrec;
2340     my $isbn = Business::ISBN->new( $isbn_string );
2341
2342     if (defined $isbn && $isbn->is_valid)
2343     {
2344         if ( ref($bibrec) ) {
2345             $bibrec->{isbn} = $isbn_string;
2346             $bibrec->{title} = undef;
2347         } else {
2348             $bibrec = { isbn => $isbn_string };
2349         }
2350     }
2351     else {
2352         $bibrec = { title => $bibrec } if !ref $bibrec;
2353     }
2354     my $array = [];
2355     for my $field (qw/ lccn isbn issn title author dewey subject /)
2356     {
2357         push @$array, { name => $field, value => $bibrec->{$field} }
2358           if defined $bibrec->{$field};
2359     }
2360     return $array;
2361 }
2362
2363 =head2 GetDistinctValues($field);
2364
2365 C<$field> is a reference to the fields array
2366
2367 =cut
2368
2369 sub GetDistinctValues {
2370     my ($fieldname,$string)=@_;
2371     # returns a reference to a hash of references to branches...
2372     if ($fieldname=~/\./){
2373                         my ($table,$column)=split /\./, $fieldname;
2374                         my $dbh = C4::Context->dbh;
2375                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2376                         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 ");
2377                         $sth->execute;
2378                         my $elements=$sth->fetchall_arrayref({});
2379                         return $elements;
2380    }
2381    else {
2382                 $string||= qq("");
2383                 my @servers=qw<biblioserver authorityserver>;
2384                 my (@zconns,@results);
2385         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2386                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2387                         $results[$i] =
2388                       $zconns[$i]->scan(
2389                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2390                       );
2391                 }
2392                 # The big moment: asynchronously retrieve results from all servers
2393                 my @elements;
2394         _ZOOM_event_loop(
2395             \@zconns,
2396             \@results,
2397             sub {
2398                 my ( $i, $size ) = @_;
2399                 for ( my $j = 0 ; $j < $size ; $j++ ) {
2400                     my %hashscan;
2401                     @hashscan{qw(value cnt)} =
2402                       $results[ $i - 1 ]->display_term($j);
2403                     push @elements, \%hashscan;
2404                 }
2405             }
2406         );
2407                 return \@elements;
2408    }
2409 }
2410
2411 =head2 _ZOOM_event_loop
2412
2413     _ZOOM_event_loop(\@zconns, \@results, sub {
2414         my ( $i, $size ) = @_;
2415         ....
2416     } );
2417
2418 Processes a ZOOM event loop and passes control to a closure for
2419 processing the results, and destroying the resultsets.
2420
2421 =cut
2422
2423 sub _ZOOM_event_loop {
2424     my ($zconns, $results, $callback) = @_;
2425     while ( ( my $i = ZOOM::event( $zconns ) ) != 0 ) {
2426         my $ev = $zconns->[ $i - 1 ]->last_event();
2427         if ( $ev == ZOOM::Event::ZEND ) {
2428             next unless $results->[ $i - 1 ];
2429             my $size = $results->[ $i - 1 ]->size();
2430             if ( $size > 0 ) {
2431                 $callback->($i, $size);
2432             }
2433         }
2434     }
2435
2436     foreach my $result (@$results) {
2437         $result->destroy();
2438     }
2439 }
2440
2441 =head2 new_record_from_zebra
2442
2443 Given raw data from a Zebra result set, return a MARC::Record object
2444
2445 This helper function is needed to take into account all the involved
2446 system preferences and configuration variables to properly create the
2447 MARC::Record object.
2448
2449 If we are using GRS-1, then the raw data we get from Zebra should be USMARC
2450 data. If we are using DOM, then it has to be MARCXML.
2451
2452 =cut
2453
2454 sub new_record_from_zebra {
2455
2456     my $server   = shift;
2457     my $raw_data = shift;
2458     # Set the default indexing modes
2459     my $index_mode = ( $server eq 'biblioserver' )
2460                         ? C4::Context->config('zebra_bib_index_mode') // 'grs1'
2461                         : C4::Context->config('zebra_auth_index_mode') // 'dom';
2462
2463     my $marc_record =  eval {
2464         if ( $index_mode eq 'dom' ) {
2465             MARC::Record->new_from_xml( $raw_data, 'UTF-8' );
2466         } else {
2467             MARC::Record->new_from_usmarc( $raw_data );
2468         }
2469     };
2470
2471     if ($@) {
2472         return;
2473     } else {
2474         return $marc_record;
2475     }
2476
2477 }
2478
2479 END { }    # module clean-up code here (global destructor)
2480
2481 1;
2482 __END__
2483
2484 =head1 AUTHOR
2485
2486 Koha Development Team <http://koha-community.org/>
2487
2488 =cut