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