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