Bug 33217: Allow specifiying sort field and order for authro links
[koha.git] / C4 / XSLT.pm
1 package C4::XSLT;
2
3 # Copyright (C) 2006 LibLime
4 # <jmf at liblime dot com>
5 # Parts Copyright Katrin Fischer 2011
6 # Parts Copyright ByWater Solutions 2011
7 # Parts Copyright Biblibre 2012
8 #
9 # This file is part of Koha.
10 #
11 # Koha is free software; you can redistribute it and/or modify it
12 # under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # Koha is distributed in the hope that it will be useful, but
17 # WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with Koha; if not, see <http://www.gnu.org/licenses>.
23
24 use Modern::Perl;
25
26 use C4::Context;
27 use C4::Koha qw( xml_escape );
28 use C4::Biblio qw( GetAuthorisedValueDesc GetFrameworkCode GetMarcStructure );
29 use Koha::AuthorisedValues;
30 use Koha::ItemTypes;
31 use Koha::RecordProcessor;
32 use Koha::XSLT::Base;
33 use Koha::Libraries;
34 use Koha::Recalls;
35
36 my $engine; #XSLT Handler object
37
38 our (@ISA, @EXPORT_OK);
39 BEGIN {
40     require Exporter;
41     @ISA = qw(Exporter);
42     @EXPORT_OK = qw(
43         buildKohaItemsNamespace
44         XSLTParse4Display
45     );
46     $engine=Koha::XSLT::Base->new( { do_not_return_source => 1 } );
47 }
48
49 =head1 NAME
50
51 C4::XSLT - Functions for displaying XSLT-generated content
52
53 =head1 FUNCTIONS
54
55 =head2 XSLTParse4Display
56
57 Returns xml for biblionumber and requested XSLT transformation.
58 Returns undef if the transform fails.
59
60 Used in OPAC results and detail, intranet results and detail, list display.
61 (Depending on the settings of your XSLT preferences.)
62
63 The helper function _get_best_default_xslt_filename is used in a unit test.
64
65 =cut
66
67 sub _get_best_default_xslt_filename {
68     my ($htdocs, $theme, $lang, $base_xslfile) = @_;
69
70     my @candidates = (
71         "$htdocs/$theme/$lang/xslt/${base_xslfile}", # exact match
72         "$htdocs/$theme/en/xslt/${base_xslfile}",    # if not, preferred theme in English
73         "$htdocs/prog/$lang/xslt/${base_xslfile}",   # if not, 'prog' theme in preferred language
74         "$htdocs/prog/en/xslt/${base_xslfile}",      # otherwise, prog theme in English; should always
75                                                      # exist
76     );
77     my $xslfilename;
78     foreach my $filename (@candidates) {
79         $xslfilename = $filename;
80         if (-f $filename) {
81             last; # we have a winner!
82         }
83     }
84     return $xslfilename;
85 }
86
87 sub get_xslt_sysprefs {
88     my $sysxml = "<sysprefs>\n";
89     foreach my $syspref ( qw/ hidelostitems OPACURLOpenInNewWindow
90                               DisplayOPACiconsXSLT URLLinkText viewISBD
91                               OPACBaseURL TraceCompleteSubfields UseICUStyleQuotes
92                               UseAuthoritiesForTracings TraceSubjectSubdivisions
93                               Display856uAsImage OPACDisplay856uAsImage 
94                               UseControlNumber IntranetBiblioDefaultView BiblioDefaultView
95                               OPACItemLocation DisplayIconsXSLT
96                               AlternateHoldingsField AlternateHoldingsSeparator
97                               TrackClicks opacthemes IdRef OpacSuppression
98                               OPACResultsLibrary OPACShowOpenURL
99                               OpenURLResolverURL OpenURLImageLocation
100                               OPACResultsMaxItems OPACResultsMaxItemsUnavailable OPACResultsUnavailableGroupingBy
101                               OpenURLText OPACShowMusicalInscripts OPACPlayMusicalInscripts ContentWarningField
102                               AuthorLinkSortBy AuthorLinkSortOrder / )
103     {
104         my $sp = C4::Context->preference( $syspref );
105         next unless defined($sp);
106         $sysxml .= "<syspref name=\"$syspref\">$sp</syspref>\n";
107     }
108
109     # singleBranchMode was a system preference, but no longer is
110     # we can retain it here for compatibility
111     my $singleBranchMode = Koha::Libraries->search->count == 1 ? 1 : 0;
112     $sysxml .= "<syspref name=\"singleBranchMode\">$singleBranchMode</syspref>\n";
113
114     $sysxml .= "</sysprefs>\n";
115     return $sysxml;
116 }
117
118 sub get_xsl_filename {
119     my ( $xslsyspref ) = @_;
120
121     my $lang   = C4::Languages::getlanguage();
122
123     my $xslfilename = C4::Context->preference($xslsyspref) || "default";
124
125     if ( $xslfilename =~ /^\s*"?default"?\s*$/i ) {
126
127         my ( $htdocs, $theme, $xslfile );
128
129         if ($xslsyspref eq "XSLTDetailsDisplay") {
130             $htdocs  = C4::Context->config('intrahtdocs');
131             $theme   = C4::Context->preference("template");
132             $xslfile = C4::Context->preference('marcflavour') .
133                        "slim2intranetDetail.xsl";
134         } elsif ($xslsyspref eq "XSLTResultsDisplay") {
135             $htdocs  = C4::Context->config('intrahtdocs');
136             $theme   = C4::Context->preference("template");
137             $xslfile = C4::Context->preference('marcflavour') .
138                         "slim2intranetResults.xsl";
139         } elsif ($xslsyspref eq "OPACXSLTDetailsDisplay") {
140             $htdocs  = C4::Context->config('opachtdocs');
141             $theme   = C4::Context->preference("opacthemes");
142             $xslfile = C4::Context->preference('marcflavour') .
143                        "slim2OPACDetail.xsl";
144         } elsif ($xslsyspref eq "OPACXSLTResultsDisplay") {
145             $htdocs  = C4::Context->config('opachtdocs');
146             $theme   = C4::Context->preference("opacthemes");
147             $xslfile = C4::Context->preference('marcflavour') .
148                        "slim2OPACResults.xsl";
149         } elsif ($xslsyspref eq 'XSLTListsDisplay') {
150             # Lists default to *Results.xslt
151             $htdocs  = C4::Context->config('intrahtdocs');
152             $theme   = C4::Context->preference("template");
153             $xslfile = C4::Context->preference('marcflavour') .
154                         "slim2intranetResults.xsl";
155         } elsif ($xslsyspref eq 'OPACXSLTListsDisplay') {
156             # Lists default to *Results.xslt
157             $htdocs  = C4::Context->config('opachtdocs');
158             $theme   = C4::Context->preference("opacthemes");
159             $xslfile = C4::Context->preference('marcflavour') .
160                        "slim2OPACResults.xsl";
161         }
162         $xslfilename = _get_best_default_xslt_filename($htdocs, $theme, $lang, $xslfile);
163     }
164
165     if ( $xslfilename =~ m/\{langcode\}/ ) {
166         $xslfilename =~ s/\{langcode\}/$lang/;
167     }
168
169     return $xslfilename;
170 }
171
172 sub XSLTParse4Display {
173     my ( $params ) = @_;
174
175     my $biblionumber = $params->{biblionumber};
176     my $record       = $params->{record};
177     my $xslsyspref   = $params->{xsl_syspref};
178     my $fixamps      = $params->{fix_amps};
179     my $hidden_items = $params->{hidden_items} || [];
180     my $variables    = $params->{xslt_variables};
181     my $items_rs     = $params->{items_rs};
182     my $interface    = C4::Context->interface;
183
184     die "Mandatory \$params->{xsl_syspref} was not provided, called with biblionumber $params->{biblionumber}"
185         if not defined $params->{xsl_syspref};
186
187     my $xslfilename = get_xsl_filename( $xslsyspref);
188
189     my $frameworkcode = GetFrameworkCode($biblionumber) || '';
190     my $record_processor = Koha::RecordProcessor->new(
191         {
192             filters => [ 'ExpandCodedFields' ],
193             options => {
194                 interface     => $interface,
195                 frameworkcode => $frameworkcode
196             }
197         }
198     );
199     $record_processor->process($record);
200
201     # grab the XML, run it through our stylesheet, push it out to the browser
202     my $itemsxml;
203     if ( $xslsyspref eq "OPACXSLTDetailsDisplay" || $xslsyspref eq "XSLTDetailsDisplay" || $xslsyspref eq "XSLTResultsDisplay" ) {
204         $itemsxml = ""; #We don't use XSLT for items display on these pages
205     } else {
206         $itemsxml = buildKohaItemsNamespace($biblionumber, $hidden_items, $items_rs);
207     }
208     my $xmlrecord = $record->as_xml(C4::Context->preference('marcflavour'));
209
210     $variables ||= {};
211     my $biblio;
212     if ( $interface eq 'opac' && C4::Context->preference('OPACShowOpenURL')) {
213         my @biblio_itemtypes;
214         $biblio //= Koha::Biblios->find($biblionumber);
215         if (C4::Context->preference('item-level_itypes')) {
216             @biblio_itemtypes = $biblio->items->get_column("itype");
217         } else {
218             push @biblio_itemtypes, $biblio->itemtype;
219         }
220         my @itypes = split( /\s/, C4::Context->preference('OPACOpenURLItemTypes') );
221         my %original = ();
222         map { $original{$_} = 1 } @biblio_itemtypes;
223         if ( grep { $original{$_} } @itypes ) {
224             $variables->{OpenURLResolverURL} = $biblio->get_openurl;
225         }
226     }
227
228     # embed variables
229     my $varxml = "<variables>\n";
230     while (my ($key, $value) = each %$variables) {
231         $value //= q{};
232         $varxml .= "<variable name=\"$key\">$value</variable>\n";
233     }
234     $varxml .= "</variables>\n";
235
236     my $sysxml = get_xslt_sysprefs();
237     $xmlrecord =~ s/\<\/record\>/$itemsxml$sysxml$varxml\<\/record\>/;
238     if ($fixamps) { # We need to correct the ampersand entities that Zebra outputs
239         $xmlrecord =~ s/\&amp;amp;/\&amp;/g;
240         $xmlrecord =~ s/\&amp\;lt\;/\&lt\;/g;
241         $xmlrecord =~ s/\&amp\;gt\;/\&gt\;/g;
242     }
243     $xmlrecord =~ s/\& /\&amp\; /;
244     $xmlrecord =~ s/\&amp\;amp\; /\&amp\; /;
245
246     #If the xslt should fail, we will return undef (old behavior was
247     #raw MARC)
248     #Note that we did set do_not_return_source at object construction
249     return $engine->transform($xmlrecord, $xslfilename ); #file or URL
250 }
251
252 =head2 buildKohaItemsNamespace
253
254     my $items_xml = buildKohaItemsNamespace( $biblionumber, [ $hidden_items, $items ] );
255
256 Returns XML for items. It accepts two optional parameters:
257 - I<$hidden_items>: An arrayref of itemnumber values, for items that should be hidden
258 - I<$items>: A Koha::Items resultset, for the items to be returned
259
260 If both parameters are passed, I<$items> is used as the basis resultset, and I<$hidden_items>
261 are filtered out of it.
262
263 Is only used in this module currently.
264
265 =cut
266
267 sub buildKohaItemsNamespace {
268     my ($biblionumber, $hidden_items, $items_rs) = @_;
269
270     $hidden_items ||= [];
271
272     my $query = {};
273     $query = { 'me.itemnumber' => { not_in => $hidden_items } }
274       if $hidden_items;
275
276     unless ( $items_rs && ref($items_rs) eq 'Koha::Items' ) {
277         $query->{'me.biblionumber'} = $biblionumber;
278         $items_rs = Koha::Items->new;
279     }
280
281     my $items = $items_rs->search( $query, { prefetch => [ 'branchtransfers', 'reserves' ] } );
282
283     my $shelflocations =
284       { map { $_->{authorised_value} => $_->{opac_description} } Koha::AuthorisedValues->get_descriptions_by_koha_field( { frameworkcode => "", kohafield => 'items.location' } ) };
285     my $ccodes =
286       { map { $_->{authorised_value} => $_->{opac_description} } Koha::AuthorisedValues->get_descriptions_by_koha_field( { frameworkcode => "", kohafield => 'items.ccode' } ) };
287
288     my %branches = map { $_->branchcode => $_->branchname } Koha::Libraries->search({}, { order_by => 'branchname' })->as_list;
289
290     my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search->unblessed } };
291     my $xml = '';
292     my %descs = map { $_->{authorised_value} => $_ } Koha::AuthorisedValues->get_descriptions_by_koha_field( { kohafield => 'items.notforloan' } );
293     my $ref_status = C4::Context->preference('Reference_NFL_Statuses') // q{}; # fallback to 1|2 removed
294
295     while ( my $item = $items->next ) {
296         my $status;
297         my $substatus = '';
298         my $recalls_count;
299
300         if ( C4::Context->preference('UseRecalls') ) {
301             $recalls_count = Koha::Recalls->search({ item_id => $item->itemnumber, status => 'waiting' })->count;
302         }
303
304         if ($recalls_count) {
305             # recalls take priority over holds
306             $status = 'other';
307             $substatus = 'Recall waiting';
308         }
309         elsif ( $item->has_pending_hold ) {
310             $status = 'other';
311             $substatus = 'Pending hold';
312         }
313         elsif ( $item->holds->waiting->count ) {
314             $status = 'other';
315             $substatus = 'Hold waiting';
316         }
317         elsif ($item->get_transfer) {
318             $status = 'other';
319             $substatus = 'In transit';
320         }
321         elsif ($item->damaged) {
322             $status = 'other';
323             $substatus = "Damaged";
324         }
325         elsif ($item->itemlost) {
326             $status = 'other';
327             $substatus = "Lost";
328         }
329         elsif ( $item->withdrawn) {
330             $status = 'other';
331             $substatus = "Withdrawn";
332         }
333         elsif ($item->onloan) {
334             $status = 'other';
335             $substatus = "Checked out";
336         }
337         elsif ( $item->notforloan ) {
338             $status = $item->notforloan =~ /^($ref_status)$/
339                 ? "reference"
340                 : "reallynotforloan";
341             $substatus = exists $descs{$item->notforloan} ? $descs{$item->notforloan}->{opac_description} : "Not for loan";
342         }
343         elsif ( exists $itemtypes->{ $item->effective_itemtype }
344             && $itemtypes->{ $item->effective_itemtype }->{notforloan}
345             && $itemtypes->{ $item->effective_itemtype }->{notforloan} == 1 )
346         {
347             $status = "1" =~ /^($ref_status)$/
348                 ? "reference"
349                 : "reallynotforloan";
350             $substatus = "Not for loan";
351         }
352         else {
353             $status = "available";
354         }
355         my $homebranch     = C4::Koha::xml_escape($branches{$item->homebranch});
356         my $holdingbranch  = C4::Koha::xml_escape($branches{$item->holdingbranch});
357         my $resultbranch   = C4::Context->preference('OPACResultsLibrary') eq 'homebranch' ? $homebranch : $holdingbranch;
358         my $location       = C4::Koha::xml_escape($item->location && exists $shelflocations->{$item->location} ? $shelflocations->{$item->location} : $item->location);
359         my $ccode          = C4::Koha::xml_escape($item->ccode    && exists $ccodes->{$item->ccode}            ? $ccodes->{$item->ccode}            : $item->ccode);
360         my $itemcallnumber = C4::Koha::xml_escape($item->itemcallnumber);
361         my $stocknumber    = C4::Koha::xml_escape($item->stocknumber);
362         $xml .=
363             "<item>"
364           . "<homebranch>$homebranch</homebranch>"
365           . "<holdingbranch>$holdingbranch</holdingbranch>"
366           . "<resultbranch>$resultbranch</resultbranch>"
367           . "<location>$location</location>"
368           . "<ccode>$ccode</ccode>"
369           . "<status>".( $status // q{} )."</status>"
370           . "<substatus>$substatus</substatus>"
371           . "<itemcallnumber>$itemcallnumber</itemcallnumber>"
372           . "<stocknumber>$stocknumber</stocknumber>"
373           . "</item>";
374     }
375     $xml = "<items xmlns=\"http://www.koha-community.org/items\">".$xml."</items>";
376     return $xml;
377 }
378
379 =head2 engine
380
381 Returns reference to XSLT handler object.
382
383 =cut
384
385 sub engine {
386     return $engine;
387 }
388
389 1;
390
391 __END__
392
393 =head1 AUTHOR
394
395 Joshua Ferraro <jmf@liblime.com>
396
397 Koha Development Team <http://koha-community.org/>
398
399 =cut