Bug 35343: Add record accessor method to Koha::Authority
[koha.git] / catalogue / search.pl
1 #!/usr/bin/perl
2 # Script to perform searching
3 # For documentation try 'perldoc /path/to/search'
4 #
5 # Copyright 2006 LibLime
6 # Copyright 2010 BibLibre
7 #
8 # This file is part of Koha
9 #
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22
23 =head1 NAME
24
25 search - a search script for finding records in a Koha system (Version 3)
26
27 =head1 OVERVIEW
28
29 This script utilizes a new search API for Koha 3. It is designed to be 
30 simple to use and configure, yet capable of performing feats like stemming,
31 field weighting, relevance ranking, support for multiple  query language
32 formats (CCL, CQL, PQF), full support for the bib1 attribute set, extended
33 attribute sets defined in Zebra profiles, access to the full range of Z39.50
34 and SRU query options, federated searches on Z39.50/SRU targets, etc.
35
36 The API as represented in this script is mostly sound, even if the individual
37 functions in Search.pm and Koha.pm need to be cleaned up. Of course, you are
38 free to disagree :-)
39
40 I will attempt to describe what is happening at each part of this script.
41 -- Joshua Ferraro <jmf AT liblime DOT com>
42
43 =head2 INTRO
44
45 This script performs two functions:
46
47 =over 
48
49 =item 1. interacts with Koha to retrieve and display the results of a search
50
51 =item 2. loads the advanced search page
52
53 =back
54
55 These two functions share many of the same variables and modules, so the first
56 task is to load what they have in common and determine which template to use.
57 Once determined, proceed to only load the variables and procedures necessary
58 for that function.
59
60 =head2 LOADING ADVANCED SEARCH PAGE
61
62 This is fairly straightforward, and I won't go into detail ;-)
63
64 =head2 PERFORMING A SEARCH
65
66 If we're performing a search, this script  performs three primary
67 operations:
68
69 =over 
70
71 =item 1. builds query strings (yes, plural)
72
73 =item 2. perform the search and return the results array
74
75 =item 3. build the HTML for output to the template
76
77 =back
78
79 There are several additional secondary functions performed that I will
80 not cover in detail.
81
82 =head3 1. Building Query Strings
83
84 There are several types of queries needed in the process of search and retrieve:
85
86 =over
87
88 =item 1 $query - the fully-built query passed to zebra
89
90 This is the most complex query that needs to be built. The original design goal 
91 was to use a custom CCL2PQF query parser to translate an incoming CCL query into
92 a multi-leaf query to pass to Zebra. It needs to be multi-leaf to allow field 
93 weighting, koha-specific relevance ranking, and stemming. When I have a chance 
94 I'll try to flesh out this section to better explain.
95
96 This query incorporates query profiles that aren't compatible with most non-Zebra 
97 Z39.50 targets to accomplish the field weighting and relevance ranking.
98
99 =item 2 $simple_query - a simple query that doesn't contain the field weighting,
100 stemming, etc., suitable to pass off to other search targets
101
102 This query is just the user's query expressed in CCL CQL, or PQF for passing to a 
103 non-zebra Z39.50 target (one that doesn't support the extended profile that Zebra does).
104
105 =item 3 $query_cgi - passed to the template / saved for future refinements of 
106 the query (by user)
107
108 This is a simple string that completely expresses the query as a CGI string that
109 can be used for future refinements of the query or as a part of a history feature.
110
111 =item 4 $query_desc - Human search description - what the user sees in search
112 feedback area
113
114 This is a simple string that is human readable. It will contain '=', ',', etc.
115
116 =back
117
118 =head3 2. Perform the Search
119
120 This section takes the query strings and performs searches on the named servers,
121 including the Koha Zebra server, stores the results in a deeply nested object, 
122 builds 'faceted results', and returns these objects.
123
124 =head3 3. Build HTML
125
126 The final major section of this script takes the objects collected thusfar and 
127 builds the HTML for output to the template and user.
128
129 =head3 Additional Notes
130
131 Not yet completed...
132
133 =cut
134
135 use Modern::Perl;
136
137 ## STEP 1. Load things that are used in both search page and
138 # results page and decide which template to load, operations 
139 # to perform, etc.
140
141 ## load Koha modules
142 use C4::Context;
143 use C4::Output qw( output_html_with_http_headers pagination_bar );
144 use C4::Circulation qw( barcodedecode );
145 use C4::Auth qw( get_template_and_user );
146 use C4::Search qw( searchResults enabled_staff_search_views z3950_search_args new_record_from_zebra );
147 use C4::Languages qw( getlanguage getLanguages );
148 use C4::Koha qw( getitemtypeimagelocation GetAuthorisedValues );
149 use URI::Escape;
150 use POSIX qw(ceil floor);
151 use C4::Search qw( searchResults enabled_staff_search_views z3950_search_args new_record_from_zebra );
152
153 use Koha::ItemTypes;
154 use Koha::Library::Groups;
155 use Koha::Patrons;
156 use Koha::SearchEngine::Search;
157 use Koha::SearchEngine::QueryBuilder;
158 use Koha::Virtualshelves;
159 use Koha::SearchFields;
160 use Koha::SearchFilters;
161
162 use URI::Escape;
163 use JSON qw( decode_json encode_json );
164
165 my $DisplayMultiPlaceHold = C4::Context->preference("DisplayMultiPlaceHold");
166 # create a new CGI object
167 # FIXME: no_undef_params needs to be tested
168 use CGI qw('-no_undef_params' -utf8 );
169 my $cgi = CGI->new;
170
171 # decide which template to use
172 my $template_name;
173 my $template_type;
174 # limits are used to limit to results to a pre-defined category such as branch or language
175 my @limits = map uri_unescape($_), $cgi->multi_param("limit");
176 my @nolimits = map uri_unescape($_), $cgi->multi_param('nolimit');
177 my %is_nolimit = map { $_ => 1 } @nolimits;
178 @limits = grep { not $is_nolimit{$_} } @limits;
179 if  (
180         !$cgi->param('edit_search') && !$cgi->param('edit_filter') &&
181         ( (@limits>=1) || (defined $cgi->param("q") && $cgi->param("q") ne "" ) || ($cgi->param('limit-yr')) )
182     ) {
183     $template_name = 'catalogue/results.tt';
184     $template_type = 'results';
185 }
186 else {
187     $template_name = 'catalogue/advsearch.tt';
188     $template_type = 'advsearch';
189 }
190 # load the template
191 my ($template, $borrowernumber, $cookie) = get_template_and_user({
192     template_name => $template_name,
193     query => $cgi,
194     type => "intranet",
195     flagsrequired   => { catalogue => 1 },
196     }
197 );
198
199 my $lang = C4::Languages::getlanguage($cgi);
200
201 if (C4::Context->preference("marcflavour") eq "UNIMARC" ) {
202     $template->param('UNIMARC' => 1);
203 }
204
205 if($cgi->cookie("holdfor")){ 
206     my $holdfor_patron = Koha::Patrons->find( $cgi->cookie("holdfor") );
207     if ( $holdfor_patron ) { # may have been deleted in the meanwhile
208         $template->param(
209             holdfor        => $cgi->cookie("holdfor"),
210             holdfor_patron => $holdfor_patron,
211         );
212     }
213 }
214
215 if($cgi->cookie("holdforclub")){
216     my $holdfor_club = Koha::Clubs->find( $cgi->cookie("holdforclub") );
217     if ( $holdfor_club ) { # May have been deleted in the meanwhile
218         $template->param(
219             holdforclub => $cgi->cookie("holdforclub"),
220             holdforclub_name => $holdfor_club->name,
221         );
222     }
223 }
224
225 if($cgi->cookie("searchToOrder")){
226     my ( $basketno, $vendorid ) = split( /\//, $cgi->cookie("searchToOrder") );
227     $template->param(
228         searchtoorder_basketno => $basketno,
229         searchtoorder_vendorid => $vendorid
230     );
231 }
232
233 # get biblionumbers stored in the cart
234 my @cart_list;
235
236 if($cgi->cookie("intranet_bib_list")){
237     my $cart_list = $cgi->cookie("intranet_bib_list");
238     @cart_list = split(/\//, $cart_list);
239 }
240
241 my @search_groups =
242   Koha::Library::Groups->get_search_groups( { interface => 'staff' } )->as_list;
243
244 my $branch_limit = '';
245 my $limit_param = $cgi->param('limit');
246 if ( $limit_param and $limit_param =~ /branch:([\w-]+)/ ) {
247     $branch_limit = $1;
248 }
249
250 $template->param(
251     search_groups    => \@search_groups,
252     branch_limit     => $branch_limit
253 );
254
255 # load the Type stuff
256 my $types = C4::Context->preference("AdvancedSearchTypes") || "itemtypes";
257 my $advancedsearchesloop = prepare_adv_search_types($types);
258 $template->param(advancedsearchesloop => $advancedsearchesloop);
259
260 $template->param( searchid => scalar $cgi->param('searchid'), );
261
262 my $default_sort_by = C4::Context->default_catalog_sort_by;
263
264 # The following should only be loaded if we're bringing up the advanced search template
265 if ( $template_type eq 'advsearch' ) {
266
267     my @operands;
268     my @operators;
269     my @indexes;
270     my $expanded = $cgi->param('expanded_options');
271     if( $cgi->param('edit_search') ){
272         @operands = $cgi->multi_param('q');
273         @operators = $cgi->multi_param('op');
274         @indexes   = $cgi->multi_param('idx');
275         $template->param(
276            sort      => $cgi->param('sort_by'),
277         );
278         # determine what to display next to the search boxes
279     } elsif ( $cgi->param('edit_filter') ){
280         my $search_filter = Koha::SearchFilters->find( $cgi->param('edit_filter') );
281         if( $search_filter ){
282             my $query = decode_json( $search_filter->query );
283             my $limits = decode_json( $search_filter->limits );
284             @operands  = @{ $query->{operands} };
285             @indexes   = @{ $query->{indexes} };
286             @operators = @{ $query->{operators} };
287             @limits    = @{ $limits->{limits} };
288             $template->param( edit_filter => $search_filter );
289         } else {
290             $template->param( unknown_filter => 1 );
291         }
292     }
293
294     while( scalar @operands < 3 ){
295         push @operands, "";
296     }
297     $template->param( operands  => \@operands );
298     $template->param( operators => \@operators );
299     $template->param( indexes   => \@indexes );
300
301     my %limit_hash;
302     foreach my $limit (@limits){
303         if ( $limit eq 'available' ){
304             $template->param( limit_available => 1 );
305         } else {
306             my ($index,$value) = split(':',$limit);
307             $value =~ s/"//g;
308             if ( $index =~ /mc-/ ){
309                 $limit_hash{$index . "_" . $value} = 1;
310             } else {
311                 push @{$limit_hash{$index}}, $value;
312             }
313         }
314     };
315     $template->param( limits => \%limit_hash );
316
317     $expanded = 1 if scalar @operators || scalar @limits;
318
319     # load the servers (used for searching -- to do federated searching, etc.)
320     my $primary_servers_loop;# = displayPrimaryServers();
321     $template->param(outer_servers_loop =>  $primary_servers_loop,);
322     
323     my $secondary_servers_loop;
324     $template->param(outer_sup_servers_loop => $secondary_servers_loop,);
325
326     # set the default sorting
327     if ($default_sort_by) {
328         $template->param( sort_by => $default_sort_by );
329     }
330
331     $template->param(uc(C4::Context->preference("marcflavour")) =>1 );
332
333     # load the language limits (for search)
334     my $languages_limit_loop = getLanguages($lang, 1);
335     $template->param(search_languages_loop => $languages_limit_loop,);
336
337     # Expanded search options in advanced search:
338     # use the global setting by default, but let the user override it
339     {
340         $expanded = C4::Context->preference("expandedSearchOption") || 0
341             if !defined($expanded) || $expanded !~ /^0|1$/;
342         $template->param( expanded_options => $expanded );
343     }
344
345     $template->param(virtualshelves => C4::Context->preference("virtualshelves"));
346
347     output_html_with_http_headers $cgi, $cookie, $template->output;
348     exit;
349 }
350
351 ### OK, if we're this far, we're performing a search, not just loading the advanced search page
352
353 # Fetch the paramater list as a hash in scalar context:
354 #  * returns paramater list as tied hash ref
355 #  * we can edit the values by changing the key
356 #  * multivalued CGI paramaters are returned as a packaged string separated by "\0" (null)
357 my $params = $cgi->Vars;
358 # Params that can have more than one value
359 # sort by is used to sort the query
360 # in theory can have more than one but generally there's just one
361 my @sort_by;
362 @sort_by = $cgi->multi_param('sort_by');
363 $sort_by[0] = $default_sort_by unless $sort_by[0];
364 foreach my $sort (@sort_by) {
365     $template->param($sort => 1) if $sort;
366 }
367 $template->param('sort_by' => $sort_by[0]);
368
369 # Use the servers defined, or just search our local catalog(default)
370 my @servers = $cgi->multi_param('server');
371 unless (@servers) {
372     #FIXME: this should be handled using Context.pm
373     @servers = ("biblioserver");
374     # @servers = C4::Context->config("biblioserver");
375 }
376 # operators include boolean and proximity operators and are used
377 # to evaluate multiple operands
378 my @operators = map uri_unescape($_), $cgi->multi_param('op');
379
380 # indexes are query qualifiers, like 'title', 'author', etc. They
381 # can be single or multiple parameters separated by comma: kw,right-Truncation 
382 my @indexes = map uri_unescape($_), $cgi->multi_param('idx');
383
384 # if a simple index (only one)  display the index used in the top search box
385 if ($indexes[0] && (!$indexes[1] || $params->{'scan'})) {
386     my $idx = "ms_".$indexes[0];
387     $idx =~ s/\,/comma/g;  # template toolkit doesn't like variables with a , in it
388     $idx =~ s/-/dash/g;  # template toolkit doesn't like variables with a dash in it
389     $template->param(header_pulldown => $idx);
390 }
391
392 # an operand can be a single term, a phrase, or a complete ccl query
393 my @operands = map uri_unescape($_), $cgi->multi_param('q');
394
395 # if a simple search, display the value in the search box
396 my $basic_search = 0;
397 if ($operands[0] && !$operands[1]) {
398     my $ms_query = $operands[0];
399     $ms_query =~ s/ #\S+//;
400     $template->param(ms_value => $ms_query);
401     $basic_search=1;
402 }
403
404 my $available;
405 foreach my $limit(@limits) {
406     if ($limit =~/available/) {
407         $available = 1;
408     }
409 }
410 $template->param(available => $available);
411
412 # append year limits if they exist
413 my $limit_yr;
414 my $limit_yr_value;
415 if ($params->{'limit-yr'}) {
416     if ($params->{'limit-yr'} =~ /\d{4}/) {
417         $limit_yr = "yr,st-numeric:$params->{'limit-yr'}";
418         $limit_yr_value = $params->{'limit-yr'};
419     }
420     push @limits,$limit_yr;
421     #FIXME: Should return a error to the user, incorect date format specified
422 }
423
424 # convert indexes and operands to corresponding parameter names for the z3950 search
425 # $ %z3950p will be a hash ref if the indexes are present (advacned search), otherwise undef
426 my $z3950par;
427 my $indexes2z3950 = {
428     kw=>'title', au=>'author', 'au,phr'=>'author', nb=>'isbn', ns=>'issn',
429     'lcn,phr'=>'dewey', su=>'subject', 'su,phr'=>'subject',
430     ti=>'title', 'ti,phr'=>'title', se=>'title'
431 };
432 for (my $ii = 0; $ii < @operands; ++$ii)
433 {
434     my $name = $indexes2z3950->{$indexes[$ii] || 'kw'};
435     if (defined $name && defined $operands[$ii])
436     {
437         $z3950par ||= {};
438         $z3950par->{$name} = $operands[$ii] if !exists $z3950par->{$name};
439     }
440 }
441
442
443 # Params that can only have one value
444 my $scan = $params->{'scan'};
445 my $count = C4::Context->preference('numSearchResults') || 20;
446 my $results_per_page = $params->{'count'} || $count;
447 my $offset = $params->{'offset'} || 0;
448 my $whole_record = $params->{'whole_record'} || 0;
449 my $weight_search = $params->{'advsearch'} ? $params->{'weight_search'} || 0 : 1;
450 $offset = 0 if $offset < 0;
451 my $page = $cgi->param('page') || 1;
452 #my $offset = ($page-1)*$results_per_page;
453
454 # Define some global variables
455 my ( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
456
457 my $builder = Koha::SearchEngine::QueryBuilder->new(
458     { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
459 my $searcher = Koha::SearchEngine::Search->new(
460     { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
461
462 # If index indicates the value is a barocode, we need to preproccess it before searching
463 for ( my $i = 0; $i < @operands; $i++ ) {
464     $operands[$i] = barcodedecode($operands[$i]) if (defined($indexes[$i]) && $indexes[$i] eq 'bc');
465 }
466
467 ## I. BUILD THE QUERY
468 (
469     $error,             $query, $simple_query, $query_cgi,
470     $query_desc,        $limit, $limit_cgi,    $limit_desc,
471     $query_type
472   )
473   = $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
474     \@sort_by, $scan, $lang, { weighted_fields => $weight_search, whole_record => $whole_record });
475
476 $template->param( search_query => $query ) if C4::Context->preference('DumpSearchQueryTemplate');
477
478 ## parse the query_cgi string and put it into a form suitable for <input>s
479 my @query_inputs;
480 my $scan_index_to_use;
481 my $scan_search_term_to_use;
482
483 if ($query_cgi) {
484     for my $this_cgi ( split('&', $query_cgi) ) {
485         next unless $this_cgi;
486         $this_cgi =~ m/(.*?)=(.*)/;
487         my $input_name = $1;
488         my $input_value = $2;
489         push @query_inputs, { input_name => $input_name, input_value => Encode::decode_utf8( uri_unescape( $input_value ) ) };
490         if ($input_name eq 'idx') {
491             # The form contains multiple fields, so take the first value as the scan index
492             $scan_index_to_use = $input_value unless $scan_index_to_use;
493         }
494         if (!defined $scan_search_term_to_use && $input_name eq 'q') {
495             $scan_search_term_to_use = Encode::decode_utf8( uri_unescape( $input_value ));
496         }
497     }
498 }
499
500 $template->param ( QUERY_INPUTS => \@query_inputs,
501                    scan_index_to_use => $scan_index_to_use,
502                    scan_search_term_to_use => $scan_search_term_to_use );
503
504 ## parse the limit_cgi string and put it into a form suitable for <input>s
505 my @limit_inputs;
506 my %active_filters;
507 if ($limit_cgi) {
508     for my $this_cgi ( split('&', $limit_cgi) ) {
509         next unless $this_cgi;
510         # handle special case limit-yr
511         if ($this_cgi =~ /yr,st-numeric/) {
512             push @limit_inputs, { input_name => 'limit-yr', input_value => $limit_yr_value };
513             next;
514         }
515         $this_cgi =~ m/(.*=)(.*)/;
516         my $input_name = $1;
517         my $input_value = $2;
518         $input_name =~ s/=$//;
519         push @limit_inputs, { input_name => $input_name, input_value => Encode::decode_utf8( uri_unescape($input_value) ) };
520         if( $input_value =~ /search_filter/ ){
521             my ($filter_id) = ( uri_unescape($input_value) =~ /^search_filter:(.*)$/ );
522             $active_filters{$filter_id} = 1;
523         }
524
525     }
526 }
527 $template->param ( LIMIT_INPUTS => \@limit_inputs );
528
529 ## II. DO THE SEARCH AND GET THE RESULTS
530 my $total = 0; # the total results for the whole set
531 my $facets; # this object stores the faceted results that display on the left-hand of the results page
532 my $results_hashref;
533
534 eval {
535     my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
536     ( $error, $results_hashref, $facets ) = $searcher->search_compat(
537         $query,            $simple_query, \@sort_by,       \@servers,
538         $results_per_page, $offset,       undef,           $itemtypes,
539         $query_type,       $scan
540     );
541 };
542
543 if ($@ || $error) {
544     my $query_error = q{};
545     $query_error .= $error if $error;
546     $query_error .= $@ if $@;
547     $template->param(query_error => $query_error);
548     output_html_with_http_headers $cgi, $cookie, $template->output;
549     exit;
550 }
551
552 # At this point, each server has given us a result set
553 # now we build that set for template display
554 my @sup_results_array;
555 for (my $i=0;$i<@servers;$i++) {
556     my $server = $servers[$i];
557     if ($server =~/biblioserver/) { # this is the local bibliographic server
558         my $hits = $results_hashref->{$server}->{"hits"} // 0;
559         if ( $hits == 0 && $basic_search ){
560             $operands[0] = '"'.$operands[0].'"'; #quote it
561             ## I. BUILD THE QUERY
562             (
563                 $error,             $query, $simple_query, $query_cgi,
564                 $query_desc,        $limit, $limit_cgi,    $limit_desc,
565                 $query_type
566               )
567               = $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
568                 \@sort_by, $scan, $lang, { weighted_fields => $weight_search, whole_record => $whole_record });
569             my $quoted_results_hashref;
570             eval {
571                 my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
572                 ( $error, $quoted_results_hashref, $facets ) = $searcher->search_compat(
573                     $query,            $simple_query, \@sort_by,       ['biblioserver'],
574                     $results_per_page, $offset,       undef,           $itemtypes,
575                     $query_type,       $scan
576                 );
577             };
578             my $quoted_hits = $quoted_results_hashref->{$server}->{"hits"} // 0;
579             if ( $quoted_hits ){
580                 $results_hashref->{'biblioserver'} = $quoted_results_hashref->{'biblioserver'};
581                 $hits = $quoted_hits;
582             }
583         }
584         my $page = $cgi->param('page') || 0;
585         my @newresults = searchResults({ 'interface' => 'intranet' }, $query_desc, $hits, $results_per_page, $offset, $scan,
586                                        $results_hashref->{$server}->{"RECORDS"});
587         $total = $total + $hits;
588
589         # Search history
590         if (C4::Context->preference('EnableSearchHistory')) {
591             unless ( $offset ) {
592                 my $path_info = $cgi->url(-path_info=>1);
593                 my $query_cgi_history = $cgi->url(-query=>1);
594                 $query_cgi_history =~ s/^$path_info\?//;
595                 $query_cgi_history =~ s/;/&/g;
596                 my $query_desc_history = $query_desc;
597                 $query_desc_history .= ", $limit_desc"
598                     if $limit_desc;
599
600                 C4::Search::History::add({
601                     userid => $borrowernumber,
602                     sessionid => $cgi->cookie("CGISESSID"),
603                     query_desc => $query_desc_history,
604                     query_cgi => $query_cgi_history,
605                     total => $total,
606                     type => "biblio",
607                 });
608             }
609             $template->param( EnableSearchHistory => 1 );
610         }
611
612         ## If there's just one result, redirect to the detail page unless doing an index scan
613         if ($total == 1 && !$scan) {
614             my $biblionumber = $newresults[0]->{biblionumber};
615             my $defaultview = C4::Context->preference('IntranetBiblioDefaultView');
616             my $views = { C4::Search::enabled_staff_search_views };
617             if ($defaultview eq 'isbd' && $views->{can_view_ISBD}) {
618                 print $cgi->redirect("/cgi-bin/koha/catalogue/ISBDdetail.pl?biblionumber=$biblionumber&found1=1");
619             } elsif  ($defaultview eq 'marc' && $views->{can_view_MARC}) {
620                 print $cgi->redirect("/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=$biblionumber&found1=1");
621             } elsif  ($defaultview eq 'labeled_marc' && $views->{can_view_labeledMARC}) {
622                 print $cgi->redirect("/cgi-bin/koha/catalogue/labeledMARCdetail.pl?biblionumber=$biblionumber&found1=1");
623             } else {
624                 print $cgi->redirect("/cgi-bin/koha/catalogue/detail.pl?biblionumber=$biblionumber&found1=1");
625             } 
626             exit;
627         }
628
629         # set up parameters if user wishes to re-run the search
630         # as a Z39.50 search
631         $template->param (z3950_search_params => C4::Search::z3950_search_args($z3950par || $query_desc));
632         $template->param(limit_cgi => $limit_cgi);
633         $template->param(query_cgi => $query_cgi);
634         $template->param(query_json => encode_json({
635             operators => \@operators,
636             operands => \@operands,
637             indexes => \@indexes
638         }));
639         $template->param(limit_json => encode_json({
640             limits => \@limits
641         }));
642         $template->param(query_desc => $query_desc);
643         $template->param(limit_desc => $limit_desc);
644         $template->param(offset     => $offset);
645         $template->param(offset     => $offset);
646
647
648         if ($hits) {
649             $template->param(total => $hits);
650             if ($limit_cgi) {
651                 my $limit_cgi_not_availablity = $limit_cgi;
652                 $limit_cgi_not_availablity =~ s/&limit=available//g;
653                 $template->param(limit_cgi_not_availablity => $limit_cgi_not_availablity);
654             }
655             $template->param(DisplayMultiPlaceHold => $DisplayMultiPlaceHold);
656             if ($query_desc || $limit_desc) {
657                 $template->param(searchdesc => 1);
658             }
659             $template->param(results_per_page =>  $results_per_page);
660             # must define a value for size if not present in DB
661             # in order to avoid problems generated by the default size value in TT
662             foreach my $line (@newresults) {
663                 if ( not exists $line->{'size'} ) { $line->{'size'} = "" }
664                 # while we're checking each line, see if item is in the cart
665                 if ( grep {$_ eq $line->{'biblionumber'}} @cart_list) {
666                     $line->{'incart'} = 1;
667                 }
668             }
669             my( $page_numbers, $hits_to_paginate, $pages, $current_page_number, $previous_page_offset, $next_page_offset, $last_page_offset ) =
670                 Koha::SearchEngine::Search->pagination_bar(
671                     {
672                         hits              => $hits,
673                         max_result_window => $searcher->max_result_window,
674                         results_per_page  => $results_per_page,
675                         offset            => $offset,
676                         sort_by           => \@sort_by
677                     }
678                 );
679             $template->param( hits_to_paginate => $hits_to_paginate );
680             $template->param(SEARCH_RESULTS => \@newresults);
681             # FIXME: no previous_page_offset when pages < 2
682             $template->param(   PAGE_NUMBERS => $page_numbers,
683                                 last_page_offset => $last_page_offset,
684                                 previous_page_offset => $previous_page_offset) unless $pages < 2;
685             $template->param(   next_page_offset => $next_page_offset) unless $pages eq $current_page_number;
686         }
687
688
689         # no hits
690         else {
691             $template->param(searchdesc => 1,query_desc => $query_desc,limit_desc => $limit_desc);
692         }
693
694     } # end of the if local
695
696     # asynchronously search the authority server
697     elsif ($server =~/authorityserver/) { # this is the local authority server
698         my @inner_sup_results_array;
699         for my $sup_record ( @{$results_hashref->{$server}->{"RECORDS"}} ) {
700             my $marc_record_object = C4::Search::new_record_from_zebra(
701                 'authorityserver',
702                 $sup_record
703             );
704             # warn "Authority Found: ".$marc_record_object->as_formatted();
705             push @inner_sup_results_array, {
706                 'title' => $marc_record_object->field(100)->subfield('a'),
707                 'link' => "&amp;idx=an&amp;q=".$marc_record_object->field('001')->as_string(),
708             };
709         }
710         push @sup_results_array, {  servername => $server, 
711                                     inner_sup_results_loop => \@inner_sup_results_array} if @inner_sup_results_array;
712     }
713     # FIXME: can add support for other targets as needed here
714     $template->param(           outer_sup_results_loop => \@sup_results_array);
715 } #/end of the for loop
716 #$template->param(FEDERATED_RESULTS => \@results_array);
717
718 my $gotonumber = $cgi->param('gotoNumber');
719 if ( $gotonumber && ( $gotonumber eq 'last' || $gotonumber eq 'first' ) ) {
720     $template->{'VARS'}->{'gotoNumber'} = $gotonumber;
721 }
722 $template->{'VARS'}->{'gotoPage'}   = 'detail.pl';
723 my $gotopage = $cgi->param('gotoPage');
724 $template->{'VARS'}->{'gotoPage'} = $gotopage
725   if $gotopage && $gotopage =~ m/^(ISBD|labeledMARC|MARC|more)?detail.pl$/;
726
727 for my $facet ( @$facets ) {
728     for my $entry ( @{ $facet->{facets} } ) {
729         my $index = $entry->{type_link_value};
730         my $value = $entry->{facet_link_value};
731         $entry->{active} = grep { $_->{input_value} eq qq{$index:$value} } @limit_inputs;
732     }
733 }
734
735
736 $template->param(
737     search_filters => Koha::SearchFilters->search({ staff_client => 1 }, { order_by => "name" }),
738     active_filters => \%active_filters,
739 ) if C4::Context->preference('SavedSearchFilters');
740
741 $template->param(
742             #classlist => $classlist,
743             total => $total,
744             opacfacets => 1,
745             facets_loop => $facets,
746             displayFacetCount=> C4::Context->preference('displayFacetCount')||0,
747             scan => $scan,
748             search_error => $error,
749 );
750
751 if ($query_desc || $limit_desc) {
752     $template->param(searchdesc => 1);
753 }
754
755 # VI. BUILD THE TEMPLATE
756
757 my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
758     {
759         borrowernumber => $borrowernumber,
760         add_allowed    => 1,
761         public         => 0,
762     }
763 );
764 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
765     {
766         borrowernumber => $borrowernumber,
767         add_allowed    => 1,
768         public         => 1,
769     }
770 );
771
772
773 $template->param(
774     add_to_some_private_shelves => $some_private_shelves,
775     add_to_some_public_shelves  => $some_public_shelves,
776 );
777
778 output_html_with_http_headers $cgi, $cookie, $template->output;
779
780
781 =head2 prepare_adv_search_types
782
783     my $type = C4::Context->preference("AdvancedSearchTypes") || "itemtypes";
784     my @advanced_search_types = prepare_adv_search_types($type);
785
786 Different types can be searched for in the advanced search. This takes the
787 system preference that defines these types and parses it into an arrayref for
788 the template.
789
790 "itemtypes" is handled specially, as itemtypes aren't an authorised value.
791 It also accounts for the "item-level_itypes" system preference.
792
793 =cut
794
795 sub prepare_adv_search_types {
796     my ($types) = @_;
797
798     my @advanced_search_types = split( /\|/, $types );
799
800     # the index parameter is different for item-level itemtypes
801     my $itype_or_itemtype =
802       ( C4::Context->preference("item-level_itypes") ) ? 'itype' : 'itemtype';
803     my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
804
805     my ( $cnt, @result );
806     foreach my $advanced_srch_type (@advanced_search_types) {
807         $advanced_srch_type =~ s/^\s*//;
808         $advanced_srch_type =~ s/\s*$//;
809         if ( $advanced_srch_type eq 'itemtypes' ) {
810
811        # itemtype is a special case, since it's not defined in authorized values
812             my @itypesloop;
813             foreach my $thisitemtype (
814                 sort {
815                     $itemtypes->{$a}->{'translated_description'}
816                       cmp $itemtypes->{$b}->{'translated_description'}
817                 } keys %$itemtypes
818               )
819             {
820                 my %row = (
821                     number      => $cnt++,
822                     ccl         => "$itype_or_itemtype,phr",
823                     code        => $thisitemtype,
824                     description => $itemtypes->{$thisitemtype}->{'translated_description'},
825                     imageurl    => getitemtypeimagelocation(
826                         'intranet', $itemtypes->{$thisitemtype}->{'imageurl'}
827                     ),
828                 );
829                 push @itypesloop, \%row;
830             }
831             my %search_code = (
832                 advanced_search_type => $advanced_srch_type,
833                 code_loop            => \@itypesloop
834             );
835             push @result, \%search_code;
836         }
837         else {
838             # covers all the other cases: non-itemtype authorized values
839             my $advsearchtypes = GetAuthorisedValues($advanced_srch_type);
840             my @authvalueloop;
841             for my $thisitemtype (@$advsearchtypes) {
842                 my %row = (
843                     number      => $cnt++,
844                     ccl         => $advanced_srch_type,
845                     code        => $thisitemtype->{authorised_value},
846                     description => $thisitemtype->{'lib'},
847                     imageurl    => getitemtypeimagelocation(
848                         'intranet', $thisitemtype->{'imageurl'}
849                     ),
850                 );
851                 push @authvalueloop, \%row;
852             }
853             my %search_code = (
854                 advanced_search_type => $advanced_srch_type,
855                 code_loop            => \@authvalueloop
856             );
857             push @result, \%search_code;
858         }
859     }
860     return \@result;
861 }