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