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