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