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