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