bug 6919 - implements XSLTResultsDisplay and adding both unimarc and marc21 xslt
[koha.git] / C4 / Search.pm
1 package C4::Search;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18 use strict;
19 #use warnings; FIXME - Bug 2505
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha;      # getFacets
24 use Lingua::Stem;
25 use C4::Search::PazPar2;
26 use XML::Simple;
27 use C4::Dates qw(format_date);
28 use C4::Members qw(GetHideLostItemsPreference);
29 use C4::XSLT;
30 use C4::Branch;
31 use C4::Reserves;    # 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);
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        # XSLT processing of some stuff
1846         if (!$scan && $search_context eq 'opac' && C4::Context->preference("OPACXSLTResultsDisplay")) {
1847             SetUTF8Flag($marcrecord);
1848             $debug && warn $marcrecord->as_formatted;
1849             # FIXME note that XSLTResultsDisplay (use of XSLT to format staff interface bib search results)
1850             # is not implemented yet
1851             $oldbiblio->{XSLTResultsRecord}
1852               = XSLTParse4Display($oldbiblio->{biblionumber},
1853                                   $marcrecord,
1854                                   'Results',
1855                                   $search_context,
1856                                   1, # clean up the problematic ampersand entities that Zebra outputs
1857                                   \@hiddenitems
1858                                 );
1859
1860         }
1861
1862         push( @newresults, $oldbiblio );
1863     }
1864
1865     return @newresults;
1866 }
1867
1868 =head2 SearchAcquisitions
1869     Search for acquisitions
1870 =cut
1871
1872 sub SearchAcquisitions{
1873     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1874
1875     my $dbh=C4::Context->dbh;
1876     # Variable initialization
1877     my $str=qq|
1878     SELECT marcxml
1879     FROM biblio
1880     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1881     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1882     WHERE dateaccessioned BETWEEN ? AND ?
1883     |;
1884
1885     my (@params,@loopcriteria);
1886
1887     push @params, $datebegin->output("iso");
1888     push @params, $dateend->output("iso");
1889
1890     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1891         if(C4::Context->preference("item-level_itypes")){
1892             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1893         }else{
1894             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1895         }
1896         push @params, @$itemtypes;
1897     }
1898
1899     if ($criteria =~/itemtype/){
1900         if(C4::Context->preference("item-level_itypes")){
1901             $str .= "AND items.itype=? ";
1902         }else{
1903             $str .= "AND biblioitems.itemtype=? ";
1904         }
1905
1906         if(scalar(@$itemtypes) == 0){
1907             my $itypes = GetItemTypes();
1908             for my $key (keys %$itypes){
1909                 push @$itemtypes, $key;
1910             }
1911         }
1912
1913         @loopcriteria= @$itemtypes;
1914     }elsif ($criteria=~/itemcallnumber/){
1915         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
1916                  OR items.itemcallnumber is NULL
1917                  OR items.itemcallnumber = '')";
1918
1919         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
1920     }else {
1921         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1922         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
1923     }
1924
1925     if ($orderby =~ /date_desc/){
1926         $str.=" ORDER BY dateaccessioned DESC";
1927     } else {
1928         $str.=" ORDER BY title";
1929     }
1930
1931     my $qdataacquisitions=$dbh->prepare($str);
1932
1933     my @loopacquisitions;
1934     foreach my $value(@loopcriteria){
1935         push @params,$value;
1936         my %cell;
1937         $cell{"title"}=$value;
1938         $cell{"titlecode"}=$value;
1939
1940         eval{$qdataacquisitions->execute(@params);};
1941
1942         if ($@){ warn "recentacquisitions Error :$@";}
1943         else {
1944             my @loopdata;
1945             while (my $data=$qdataacquisitions->fetchrow_hashref){
1946                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1947             }
1948             $cell{"loopdata"}=\@loopdata;
1949         }
1950         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1951         pop @params;
1952     }
1953     $qdataacquisitions->finish;
1954     return \@loopacquisitions;
1955 }
1956 #----------------------------------------------------------------------
1957 #
1958 # Non-Zebra GetRecords#
1959 #----------------------------------------------------------------------
1960
1961 =head2 NZgetRecords
1962
1963   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1964
1965 =cut
1966
1967 sub NZgetRecords {
1968     my (
1969         $query,            $simple_query, $sort_by_ref,    $servers_ref,
1970         $results_per_page, $offset,       $expanded_facet, $branches,
1971         $query_type,       $scan
1972     ) = @_;
1973     warn "query =$query" if $DEBUG;
1974     my $result = NZanalyse($query);
1975     warn "results =$result" if $DEBUG;
1976     return ( undef,
1977         NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1978         undef );
1979 }
1980
1981 =head2 NZanalyse
1982
1983   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1984   the list is built from an inverted index in the nozebra SQL table
1985   note that title is here only for convenience : the sorting will be very fast when requested on title
1986   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1987
1988 =cut
1989
1990 sub NZanalyse {
1991     my ( $string, $server ) = @_;
1992 #     warn "---------"       if $DEBUG;
1993     warn " NZanalyse" if $DEBUG;
1994 #     warn "---------"       if $DEBUG;
1995
1996  # $server contains biblioserver or authorities, depending on what we search on.
1997  #warn "querying : $string on $server";
1998     $server = 'biblioserver' unless $server;
1999
2000 # if we have a ", replace the content to discard temporarily any and/or/not inside
2001     my $commacontent;
2002     if ( $string =~ /"/ ) {
2003         $string =~ s/"(.*?)"/__X__/;
2004         $commacontent = $1;
2005         warn "commacontent : $commacontent" if $DEBUG;
2006     }
2007
2008 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
2009 # then, call again NZanalyse with $left and $right
2010 # (recursive until we find a leaf (=> something without and/or/not)
2011 # delete repeated operator... Would then go in infinite loop
2012     while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
2013     }
2014
2015     #process parenthesis before.
2016     if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
2017         my $left     = $1;
2018         my $right    = $4;
2019         my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
2020         warn
2021 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
2022           if $DEBUG;
2023         my $leftresult = NZanalyse( $left, $server );
2024         if ($operator) {
2025             my $rightresult = NZanalyse( $right, $server );
2026
2027             # OK, we have the results for right and left part of the query
2028             # depending of operand, intersect, union or exclude both lists
2029             # to get a result list
2030             if ( $operator eq ' and ' ) {
2031                 return NZoperatorAND($leftresult,$rightresult);
2032             }
2033             elsif ( $operator eq ' or ' ) {
2034
2035                 # just merge the 2 strings
2036                 return $leftresult . $rightresult;
2037             }
2038             elsif ( $operator eq ' not ' ) {
2039                 return NZoperatorNOT($leftresult,$rightresult);
2040             }
2041         }
2042         else {
2043 # this error is impossible, because of the regexp that isolate the operand, but just in case...
2044             return $leftresult;
2045         }
2046     }
2047     warn "string :" . $string if $DEBUG;
2048     my $left = "";
2049     my $right = "";
2050     my $operator = "";
2051     if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
2052         $left     = $1;
2053         $right    = $3;
2054         $operator = lc($2);    # FIXME: and/or/not are operators, not operands
2055     }
2056     warn "no parenthesis. left : $left operator: $operator right: $right"
2057       if $DEBUG;
2058
2059     # it's not a leaf, we have a and/or/not
2060     if ($operator) {
2061
2062         # reintroduce comma content if needed
2063         $right =~ s/__X__/"$commacontent"/ if $commacontent;
2064         $left  =~ s/__X__/"$commacontent"/ if $commacontent;
2065         warn "node : $left / $operator / $right\n" if $DEBUG;
2066         my $leftresult  = NZanalyse( $left,  $server );
2067         my $rightresult = NZanalyse( $right, $server );
2068         warn " leftresult : $leftresult" if $DEBUG;
2069         warn " rightresult : $rightresult" if $DEBUG;
2070         # OK, we have the results for right and left part of the query
2071         # depending of operand, intersect, union or exclude both lists
2072         # to get a result list
2073         if ( $operator eq ' and ' ) {
2074             return NZoperatorAND($leftresult,$rightresult);
2075         }
2076         elsif ( $operator eq ' or ' ) {
2077
2078             # just merge the 2 strings
2079             return $leftresult . $rightresult;
2080         }
2081         elsif ( $operator eq ' not ' ) {
2082             return NZoperatorNOT($leftresult,$rightresult);
2083         }
2084         else {
2085
2086 # this error is impossible, because of the regexp that isolate the operand, but just in case...
2087             die "error : operand unknown : $operator for $string";
2088         }
2089
2090         # it's a leaf, do the real SQL query and return the result
2091     }
2092     else {
2093         $string =~ s/__X__/"$commacontent"/ if $commacontent;
2094         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
2095         #remove trailing blank at the beginning
2096         $string =~ s/^ //g;
2097         warn "leaf:$string" if $DEBUG;
2098
2099         # parse the string in in operator/operand/value again
2100         my $left = "";
2101         my $operator = "";
2102         my $right = "";
2103         if ($string =~ /(.*)(>=|<=)(.*)/) {
2104             $left     = $1;
2105             $operator = $2;
2106             $right    = $3;
2107         } else {
2108             $left = $string;
2109         }
2110 #         warn "handling leaf... left:$left operator:$operator right:$right"
2111 #           if $DEBUG;
2112         unless ($operator) {
2113             if ($string =~ /(.*)(>|<|=)(.*)/) {
2114                 $left     = $1;
2115                 $operator = $2;
2116                 $right    = $3;
2117                 warn
2118     "handling unless (operator)... left:$left operator:$operator right:$right"
2119                 if $DEBUG;
2120             } else {
2121                 $left = $string;
2122             }
2123         }
2124         my $results;
2125
2126 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
2127         $left =~ s/ .*$//;
2128
2129         # automatic replace for short operators
2130         $left = 'title'            if $left =~ '^ti$';
2131         $left = 'author'           if $left =~ '^au$';
2132         $left = 'publisher'        if $left =~ '^pb$';
2133         $left = 'subject'          if $left =~ '^su$';
2134         $left = 'koha-Auth-Number' if $left =~ '^an$';
2135         $left = 'keyword'          if $left =~ '^kw$';
2136         $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
2137         warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
2138         my $dbh = C4::Context->dbh;
2139         if ( $operator && $left ne 'keyword' ) {
2140             #do a specific search
2141             $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
2142             my $sth = $dbh->prepare(
2143 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
2144             );
2145             warn "$left / $operator / $right\n" if $DEBUG;
2146
2147             # split each word, query the DB and build the biblionumbers result
2148             #sanitizing leftpart
2149             $left =~ s/^\s+|\s+$//;
2150             foreach ( split / /, $right ) {
2151                 my $biblionumbers;
2152                 $_ =~ s/^\s+|\s+$//;
2153                 next unless $_;
2154                 warn "EXECUTE : $server, $left, $_" if $DEBUG;
2155                 $sth->execute( $server, $left, $_ )
2156                   or warn "execute failed: $!";
2157                 while ( my ( $line, $value ) = $sth->fetchrow ) {
2158
2159 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
2160 # otherwise, fill the result
2161                     $biblionumbers .= $line
2162                       unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
2163                     warn "result : $value "
2164                       . ( $right  =~ /\d/ ) . "=="
2165                       . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
2166                 }
2167
2168 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2169                 if ($results) {
2170                     warn "NZAND" if $DEBUG;
2171                     $results = NZoperatorAND($biblionumbers,$results);
2172                 } else {
2173                     $results = $biblionumbers;
2174                 }
2175             }
2176         }
2177         else {
2178       #do a complete search (all indexes), if index='kw' do complete search too.
2179             my $sth = $dbh->prepare(
2180 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
2181             );
2182
2183             # split each word, query the DB and build the biblionumbers result
2184             foreach ( split / /, $string ) {
2185                 next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
2186                 warn "search on all indexes on $_" if $DEBUG;
2187                 my $biblionumbers;
2188                 next unless $_;
2189                 $sth->execute( $server, $_ );
2190                 while ( my $line = $sth->fetchrow ) {
2191                     $biblionumbers .= $line;
2192                 }
2193
2194 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2195                 if ($results) {
2196                     $results = NZoperatorAND($biblionumbers,$results);
2197                 }
2198                 else {
2199                     warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
2200                     $results = $biblionumbers;
2201                 }
2202             }
2203         }
2204         warn "return : $results for LEAF : $string" if $DEBUG;
2205         return $results;
2206     }
2207     warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
2208 }
2209
2210 sub NZoperatorAND{
2211     my ($rightresult, $leftresult)=@_;
2212
2213     my @leftresult = split /;/, $leftresult;
2214     warn " @leftresult / $rightresult \n" if $DEBUG;
2215
2216     #             my @rightresult = split /;/,$leftresult;
2217     my $finalresult;
2218
2219 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
2220 # the result is stored twice, to have the same weight for AND than OR.
2221 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
2222 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
2223     foreach (@leftresult) {
2224         my $value = $_;
2225         my $countvalue;
2226         ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
2227         if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
2228             $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
2229             $finalresult .=
2230                 "$value-$countvalue;$value-$countvalue;";
2231         }
2232     }
2233     warn "NZAND DONE : $finalresult \n" if $DEBUG;
2234     return $finalresult;
2235 }
2236
2237 sub NZoperatorOR{
2238     my ($rightresult, $leftresult)=@_;
2239     return $rightresult.$leftresult;
2240 }
2241
2242 sub NZoperatorNOT{
2243     my ($leftresult, $rightresult)=@_;
2244
2245     my @leftresult = split /;/, $leftresult;
2246
2247     #             my @rightresult = split /;/,$leftresult;
2248     my $finalresult;
2249     foreach (@leftresult) {
2250         my $value=$_;
2251         $value=$1 if $value=~m/(.*)-\d+$/;
2252         unless ($rightresult =~ "$value-") {
2253             $finalresult .= "$_;";
2254         }
2255     }
2256     return $finalresult;
2257 }
2258
2259 =head2 NZorder
2260
2261   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
2262
2263   TODO :: Description
2264
2265 =cut
2266
2267 sub NZorder {
2268     my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
2269     warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
2270
2271     # order title asc by default
2272     #     $ordering = '1=36 <i' unless $ordering;
2273     $results_per_page = 20 unless $results_per_page;
2274     $offset           = 0  unless $offset;
2275     my $dbh = C4::Context->dbh;
2276
2277     #
2278     # order by POPULARITY
2279     #
2280     if ( $ordering =~ /popularity/ ) {
2281         my %result;
2282         my %popularity;
2283
2284         # popularity is not in MARC record, it's builded from a specific query
2285         my $sth =
2286           $dbh->prepare("select sum(issues) from items where biblionumber=?");
2287         foreach ( split /;/, $biblionumbers ) {
2288             my ( $biblionumber, $title ) = split /,/, $_;
2289             $result{$biblionumber} = GetMarcBiblio($biblionumber);
2290             $sth->execute($biblionumber);
2291             my $popularity = $sth->fetchrow || 0;
2292
2293 # hint : the key is popularity.title because we can have
2294 # many results with the same popularity. In this case, sub-ordering is done by title
2295 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
2296 # (un-frequent, I agree, but we won't forget anything that way ;-)
2297             $popularity{ sprintf( "%10d", $popularity ) . $title
2298                   . $biblionumber } = $biblionumber;
2299         }
2300
2301     # sort the hash and return the same structure as GetRecords (Zebra querying)
2302         my $result_hash;
2303         my $numbers = 0;
2304         if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
2305             foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
2306                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2307                   $result{ $popularity{$key} }->as_usmarc();
2308             }
2309         }
2310         else {                                    # sort popularity ASC
2311             foreach my $key ( sort ( keys %popularity ) ) {
2312                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2313                   $result{ $popularity{$key} }->as_usmarc();
2314             }
2315         }
2316         my $finalresult = ();
2317         $result_hash->{'hits'}         = $numbers;
2318         $finalresult->{'biblioserver'} = $result_hash;
2319         return $finalresult;
2320
2321         #
2322         # ORDER BY author
2323         #
2324     }
2325     elsif ( $ordering =~ /author/ ) {
2326         my %result;
2327         foreach ( split /;/, $biblionumbers ) {
2328             my ( $biblionumber, $title ) = split /,/, $_;
2329             my $record = GetMarcBiblio($biblionumber);
2330             my $author;
2331             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2332                 $author = $record->subfield( '200', 'f' );
2333                 $author = $record->subfield( '700', 'a' ) unless $author;
2334             }
2335             else {
2336                 $author = $record->subfield( '100', 'a' );
2337             }
2338
2339 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2340 # and we don't want to get only 1 result for each of them !!!
2341             $result{ $author . $biblionumber } = $record;
2342         }
2343
2344     # sort the hash and return the same structure as GetRecords (Zebra querying)
2345         my $result_hash;
2346         my $numbers = 0;
2347         if ( $ordering eq 'author_za' || $ordering eq 'author_dsc' ) {    # sort by author desc
2348             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2349                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2350                   $result{$key}->as_usmarc();
2351             }
2352         }
2353         else {                               # sort by author ASC
2354             foreach my $key ( sort ( keys %result ) ) {
2355                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2356                   $result{$key}->as_usmarc();
2357             }
2358         }
2359         my $finalresult = ();
2360         $result_hash->{'hits'}         = $numbers;
2361         $finalresult->{'biblioserver'} = $result_hash;
2362         return $finalresult;
2363
2364         #
2365         # ORDER BY callnumber
2366         #
2367     }
2368     elsif ( $ordering =~ /callnumber/ ) {
2369         my %result;
2370         foreach ( split /;/, $biblionumbers ) {
2371             my ( $biblionumber, $title ) = split /,/, $_;
2372             my $record = GetMarcBiblio($biblionumber);
2373             my $callnumber;
2374             my $frameworkcode = GetFrameworkCode($biblionumber);
2375             my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField(  'items.itemcallnumber', $frameworkcode);
2376                ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
2377                 unless $callnumber_tag;
2378             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2379                 $callnumber = $record->subfield( '200', 'f' );
2380             } else {
2381                 $callnumber = $record->subfield( '100', 'a' );
2382             }
2383
2384 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2385 # and we don't want to get only 1 result for each of them !!!
2386             $result{ $callnumber . $biblionumber } = $record;
2387         }
2388
2389     # sort the hash and return the same structure as GetRecords (Zebra querying)
2390         my $result_hash;
2391         my $numbers = 0;
2392         if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
2393             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2394                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2395                   $result{$key}->as_usmarc();
2396             }
2397         }
2398         else {                                     # sort by title ASC
2399             foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2400                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2401                   $result{$key}->as_usmarc();
2402             }
2403         }
2404         my $finalresult = ();
2405         $result_hash->{'hits'}         = $numbers;
2406         $finalresult->{'biblioserver'} = $result_hash;
2407         return $finalresult;
2408     }
2409     elsif ( $ordering =~ /pubdate/ ) {             #pub year
2410         my %result;
2411         foreach ( split /;/, $biblionumbers ) {
2412             my ( $biblionumber, $title ) = split /,/, $_;
2413             my $record = GetMarcBiblio($biblionumber);
2414             my ( $publicationyear_tag, $publicationyear_subfield ) =
2415               GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2416             my $publicationyear =
2417               $record->subfield( $publicationyear_tag,
2418                 $publicationyear_subfield );
2419
2420 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2421 # and we don't want to get only 1 result for each of them !!!
2422             $result{ $publicationyear . $biblionumber } = $record;
2423         }
2424
2425     # sort the hash and return the same structure as GetRecords (Zebra querying)
2426         my $result_hash;
2427         my $numbers = 0;
2428         if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
2429             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2430                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2431                   $result{$key}->as_usmarc();
2432             }
2433         }
2434         else {                                 # sort by pub year ASC
2435             foreach my $key ( sort ( keys %result ) ) {
2436                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2437                   $result{$key}->as_usmarc();
2438             }
2439         }
2440         my $finalresult = ();
2441         $result_hash->{'hits'}         = $numbers;
2442         $finalresult->{'biblioserver'} = $result_hash;
2443         return $finalresult;
2444
2445         #
2446         # ORDER BY title
2447         #
2448     }
2449     elsif ( $ordering =~ /title/ ) {
2450
2451 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2452         my %result;
2453         foreach ( split /;/, $biblionumbers ) {
2454             my ( $biblionumber, $title ) = split /,/, $_;
2455
2456 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2457 # and we don't want to get only 1 result for each of them !!!
2458 # hint & speed improvement : we can order without reading the record
2459 # so order, and read records only for the requested page !
2460             $result{ $title . $biblionumber } = $biblionumber;
2461         }
2462
2463     # sort the hash and return the same structure as GetRecords (Zebra querying)
2464         my $result_hash;
2465         my $numbers = 0;
2466         if ( $ordering eq 'title_az' ) {    # sort by title desc
2467             foreach my $key ( sort ( keys %result ) ) {
2468                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2469             }
2470         }
2471         else {                              # sort by title ASC
2472             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2473                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2474             }
2475         }
2476
2477         # limit the $results_per_page to result size if it's more
2478         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2479
2480         # for the requested page, replace biblionumber by the complete record
2481         # speed improvement : avoid reading too much things
2482         for (
2483             my $counter = $offset ;
2484             $counter <= $offset + $results_per_page ;
2485             $counter++
2486           )
2487         {
2488             $result_hash->{'RECORDS'}[$counter] =
2489               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2490         }
2491         my $finalresult = ();
2492         $result_hash->{'hits'}         = $numbers;
2493         $finalresult->{'biblioserver'} = $result_hash;
2494         return $finalresult;
2495     }
2496     else {
2497
2498 #
2499 # order by ranking
2500 #
2501 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2502         my %result;
2503         my %count_ranking;
2504         foreach ( split /;/, $biblionumbers ) {
2505             my ( $biblionumber, $title ) = split /,/, $_;
2506             $title =~ /(.*)-(\d)/;
2507
2508             # get weight
2509             my $ranking = $2;
2510
2511 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2512 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2513 # biblio N has ranking = 6
2514             $count_ranking{$biblionumber} += $ranking;
2515         }
2516
2517 # build the result by "inverting" the count_ranking hash
2518 # 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
2519 #         warn "counting";
2520         foreach ( keys %count_ranking ) {
2521             $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2522         }
2523
2524     # sort the hash and return the same structure as GetRecords (Zebra querying)
2525         my $result_hash;
2526         my $numbers = 0;
2527         foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2528             $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2529         }
2530
2531         # limit the $results_per_page to result size if it's more
2532         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2533
2534         # for the requested page, replace biblionumber by the complete record
2535         # speed improvement : avoid reading too much things
2536         for (
2537             my $counter = $offset ;
2538             $counter <= $offset + $results_per_page ;
2539             $counter++
2540           )
2541         {
2542             $result_hash->{'RECORDS'}[$counter] =
2543               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2544               if $result_hash->{'RECORDS'}[$counter];
2545         }
2546         my $finalresult = ();
2547         $result_hash->{'hits'}         = $numbers;
2548         $finalresult->{'biblioserver'} = $result_hash;
2549         return $finalresult;
2550     }
2551 }
2552
2553 =head2 enabled_staff_search_views
2554
2555 %hash = enabled_staff_search_views()
2556
2557 This function returns a hash that contains three flags obtained from the system
2558 preferences, used to determine whether a particular staff search results view
2559 is enabled.
2560
2561 =over 2
2562
2563 =item C<Output arg:>
2564
2565     * $hash{can_view_MARC} is true only if the MARC view is enabled
2566     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2567     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2568
2569 =item C<usage in the script:>
2570
2571 =back
2572
2573 $template->param ( C4::Search::enabled_staff_search_views );
2574
2575 =cut
2576
2577 sub enabled_staff_search_views
2578 {
2579         return (
2580                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2581                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2582                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2583         );
2584 }
2585
2586 sub AddSearchHistory{
2587         my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2588     my $dbh = C4::Context->dbh;
2589
2590     # Add the request the user just made
2591     my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2592     my $sth   = $dbh->prepare($sql);
2593     $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2594         return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2595 }
2596
2597 sub GetSearchHistory{
2598         my ($borrowernumber,$session)=@_;
2599     my $dbh = C4::Context->dbh;
2600
2601     # Add the request the user just made
2602     my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2603     my $sth   = $dbh->prepare($query);
2604         $sth->execute($borrowernumber, $session);
2605     return  $sth->fetchall_hashref({});
2606 }
2607
2608 =head2 z3950_search_args
2609
2610 $arrayref = z3950_search_args($matchpoints)
2611
2612 This function returns an array reference that contains the search parameters to be
2613 passed to the Z39.50 search script (z3950_search.pl). The array elements
2614 are hash refs whose keys are name, value and encvalue, and whose values are the
2615 name of a search parameter, the value of that search parameter and the URL encoded
2616 value of that parameter.
2617
2618 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2619
2620 The search parameter values are obtained from the bibliographic record whose
2621 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2622
2623 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2624 a general purpose search argument. In this case, the returned array contains only
2625 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2626
2627 If a search parameter value is undefined or empty, it is not included in the returned
2628 array.
2629
2630 The returned array reference may be passed directly to the template parameters.
2631
2632 =over 2
2633
2634 =item C<Output arg:>
2635
2636     * $array containing hash refs as described above
2637
2638 =item C<usage in the script:>
2639
2640 =back
2641
2642 $data = Biblio::GetBiblioData($bibno);
2643 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2644
2645 *OR*
2646
2647 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2648
2649 =cut
2650
2651 sub z3950_search_args {
2652     my $bibrec = shift;
2653     my $isbn = Business::ISBN->new($bibrec);
2654
2655     if (defined $isbn && $isbn->is_valid)
2656     {
2657         $bibrec = { isbn => $bibrec } if !ref $bibrec;
2658     }
2659     else {
2660         $bibrec = { title => $bibrec } if !ref $bibrec;
2661     }
2662     my $array = [];
2663     for my $field (qw/ lccn isbn issn title author dewey subject /)
2664     {
2665         my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2666         push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2667     }
2668     return $array;
2669 }
2670
2671 =head2 BiblioAddAuthorities
2672
2673 ( $countlinked, $countcreated ) = BiblioAddAuthorities($record, $frameworkcode);
2674
2675 this function finds the authorities linked to the biblio
2676     * search in the authority DB for the same authid (in $9 of the biblio)
2677     * search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
2678     * search in the authority DB for the same values (exactly) (in all subfields of the biblio)
2679 OR adds a new authority record
2680
2681 =over 2
2682
2683 =item C<input arg:>
2684
2685     * $record is the MARC record in question (marc blob)
2686     * $frameworkcode is the bibliographic framework to use (if it is "" it uses the default framework)
2687
2688 =item C<Output arg:>
2689
2690     * $countlinked is the number of authorities records that are linked to this authority
2691     * $countcreated
2692
2693 =item C<BUGS>
2694     * 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)
2695
2696 =back
2697
2698 =cut
2699
2700
2701 sub BiblioAddAuthorities{
2702   my ( $record, $frameworkcode ) = @_;
2703   my $dbh=C4::Context->dbh;
2704   my $query=$dbh->prepare(qq|
2705 SELECT authtypecode,tagfield
2706 FROM marc_subfield_structure
2707 WHERE frameworkcode=?
2708 AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
2709 # SELECT authtypecode,tagfield
2710 # FROM marc_subfield_structure
2711 # WHERE frameworkcode=?
2712 # AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
2713   $query->execute($frameworkcode);
2714   my ($countcreated,$countlinked);
2715   while (my $data=$query->fetchrow_hashref){
2716     foreach my $field ($record->field($data->{tagfield})){
2717       next if ($field->subfield('3')||$field->subfield('9'));
2718       # No authorities id in the tag.
2719       # Search if there is any authorities to link to.
2720       my $query='at='.$data->{authtypecode}.' ';
2721       map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)}  $field->subfields();
2722       my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
2723     # there is only 1 result
2724           if ( $error ) {
2725         warn "BIBLIOADDSAUTHORITIES: $error";
2726             return (0,0) ;
2727           }
2728       if ( @{$results} == 1 ) {
2729         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2730         $field->add_subfields('9'=>$marcrecord->field('001')->data);
2731         $countlinked++;
2732       } elsif ( @{$results} > 1 ) {
2733    #More than One result
2734    #This can comes out of a lack of a subfield.
2735 #         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2736 #         $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
2737   $countlinked++;
2738       } else {
2739   #There are no results, build authority record, add it to Authorities, get authid and add it to 9
2740   ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode
2741   ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
2742          my $authtypedata=C4::AuthoritiesMarc::GetAuthType($data->{authtypecode});
2743          next unless $authtypedata;
2744          my $marcrecordauth=MARC::Record->new();
2745          my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
2746          map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )}  $field->subfields();
2747          $marcrecordauth->insert_fields_ordered($authfield);
2748
2749          # bug 2317: ensure new authority knows it's using UTF-8; currently
2750          # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
2751          # automatically for UNIMARC (by not transcoding)
2752          # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
2753          # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
2754          # of change to a core API just before the 3.0 release.
2755          if (C4::Context->preference('marcflavour') eq 'MARC21') {
2756             SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
2757          }
2758
2759 #          warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
2760
2761          my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
2762          $countcreated++;
2763          $field->add_subfields('9'=>$authid);
2764       }
2765     }
2766   }
2767   return ($countlinked,$countcreated);
2768 }
2769
2770 =head2 GetDistinctValues($field);
2771
2772 C<$field> is a reference to the fields array
2773
2774 =cut
2775
2776 sub GetDistinctValues {
2777     my ($fieldname,$string)=@_;
2778     # returns a reference to a hash of references to branches...
2779     if ($fieldname=~/\./){
2780                         my ($table,$column)=split /\./, $fieldname;
2781                         my $dbh = C4::Context->dbh;
2782                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2783                         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 ");
2784                         $sth->execute;
2785                         my $elements=$sth->fetchall_arrayref({});
2786                         return $elements;
2787    }
2788    else {
2789                 $string||= qq("");
2790                 my @servers=qw<biblioserver authorityserver>;
2791                 my (@zconns,@results);
2792         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2793                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2794                         $results[$i] =
2795                       $zconns[$i]->scan(
2796                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2797                       );
2798                 }
2799                 # The big moment: asynchronously retrieve results from all servers
2800                 my @elements;
2801                 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
2802                         my $ev = $zconns[ $i - 1 ]->last_event();
2803                         if ( $ev == ZOOM::Event::ZEND ) {
2804                                 next unless $results[ $i - 1 ];
2805                                 my $size = $results[ $i - 1 ]->size();
2806                                 if ( $size > 0 ) {
2807                       for (my $j=0;$j<$size;$j++){
2808                                                 my %hashscan;
2809                                                 @hashscan{qw(value cnt)}=$results[ $i - 1 ]->display_term($j);
2810                                                 push @elements, \%hashscan;
2811                                           }
2812                                 }
2813                         }
2814                 }
2815                 return \@elements;
2816    }
2817 }
2818
2819
2820 END { }    # module clean-up code here (global destructor)
2821
2822 1;
2823 __END__
2824
2825 =head1 AUTHOR
2826
2827 Koha Development Team <http://koha-community.org/>
2828
2829 =cut