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