Bug 33568: Fix action links display
[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 (
90         qw/ hidelostitems OPACURLOpenInNewWindow
91         DisplayOPACiconsXSLT URLLinkText viewISBD
92         OPACBaseURL TraceCompleteSubfields UseICUStyleQuotes
93         UseAuthoritiesForTracings TraceSubjectSubdivisions
94         Display856uAsImage OPACDisplay856uAsImage
95         UseControlNumber IntranetBiblioDefaultView BiblioDefaultView
96         OPACItemLocation DisplayIconsXSLT
97         AlternateHoldingsField AlternateHoldingsSeparator
98         TrackClicks opacthemes IdRef OpacSuppression
99         OPACResultsLibrary OPACShowOpenURL
100         OpenURLResolverURL OpenURLImageLocation
101         OPACResultsMaxItems OPACResultsMaxItemsUnavailable OPACResultsUnavailableGroupingBy
102         OpenURLText OPACShowMusicalInscripts OPACPlayMusicalInscripts ContentWarningField
103         AuthorLinkSortBy AuthorLinkSortOrder /
104         )
105     {
106         my $sp = C4::Context->preference($syspref);
107         next unless defined($sp);
108         $sysxml .= "<syspref name=\"$syspref\">$sp</syspref>\n";
109     }
110
111     # singleBranchMode was a system preference, but no longer is
112     # we can retain it here for compatibility
113     my $singleBranchMode = Koha::Libraries->search->count == 1 ? 1 : 0;
114     $sysxml .= "<syspref name=\"singleBranchMode\">$singleBranchMode</syspref>\n";
115
116     $sysxml .= "</sysprefs>\n";
117     return $sysxml;
118 }
119
120 sub get_xsl_filename {
121     my ( $xslsyspref ) = @_;
122
123     my $lang   = C4::Languages::getlanguage();
124
125     my $xslfilename = C4::Context->preference($xslsyspref) || "default";
126
127     if ( $xslfilename =~ /^\s*"?default"?\s*$/i ) {
128
129         my ( $htdocs, $theme, $xslfile );
130
131         if ($xslsyspref eq "XSLTDetailsDisplay") {
132             $htdocs  = C4::Context->config('intrahtdocs');
133             $theme   = C4::Context->preference("template");
134             $xslfile = C4::Context->preference('marcflavour') .
135                        "slim2intranetDetail.xsl";
136         } elsif ($xslsyspref eq "XSLTResultsDisplay") {
137             $htdocs  = C4::Context->config('intrahtdocs');
138             $theme   = C4::Context->preference("template");
139             $xslfile = C4::Context->preference('marcflavour') .
140                         "slim2intranetResults.xsl";
141         } elsif ($xslsyspref eq "OPACXSLTDetailsDisplay") {
142             $htdocs  = C4::Context->config('opachtdocs');
143             $theme   = C4::Context->preference("opacthemes");
144             $xslfile = C4::Context->preference('marcflavour') .
145                        "slim2OPACDetail.xsl";
146         } elsif ($xslsyspref eq "OPACXSLTResultsDisplay") {
147             $htdocs  = C4::Context->config('opachtdocs');
148             $theme   = C4::Context->preference("opacthemes");
149             $xslfile = C4::Context->preference('marcflavour') .
150                        "slim2OPACResults.xsl";
151         } elsif ($xslsyspref eq 'XSLTListsDisplay') {
152             # Lists default to *Results.xslt
153             $htdocs  = C4::Context->config('intrahtdocs');
154             $theme   = C4::Context->preference("template");
155             $xslfile = C4::Context->preference('marcflavour') .
156                         "slim2intranetResults.xsl";
157         } elsif ($xslsyspref eq 'OPACXSLTListsDisplay') {
158             # Lists default to *Results.xslt
159             $htdocs  = C4::Context->config('opachtdocs');
160             $theme   = C4::Context->preference("opacthemes");
161             $xslfile = C4::Context->preference('marcflavour') .
162                        "slim2OPACResults.xsl";
163         }
164         $xslfilename = _get_best_default_xslt_filename($htdocs, $theme, $lang, $xslfile);
165     }
166
167     if ( $xslfilename =~ m/\{langcode\}/ ) {
168         $xslfilename =~ s/\{langcode\}/$lang/;
169     }
170
171     return $xslfilename;
172 }
173
174 sub XSLTParse4Display {
175     my ( $params ) = @_;
176
177     my $biblionumber = $params->{biblionumber};
178     my $record       = $params->{record};
179     my $xslsyspref   = $params->{xsl_syspref};
180     my $fixamps      = $params->{fix_amps};
181     my $hidden_items = $params->{hidden_items} || [];
182     my $variables    = $params->{xslt_variables};
183     my $items_rs     = $params->{items_rs};
184     my $interface    = C4::Context->interface;
185
186     die "Mandatory \$params->{xsl_syspref} was not provided, called with biblionumber $params->{biblionumber}"
187         if not defined $params->{xsl_syspref};
188
189     my $xslfilename = get_xsl_filename( $xslsyspref);
190
191     my $frameworkcode = GetFrameworkCode($biblionumber) || '';
192     my $record_processor = Koha::RecordProcessor->new(
193         {
194             filters => [ 'ExpandCodedFields' ],
195             options => {
196                 interface     => $interface,
197                 frameworkcode => $frameworkcode
198             }
199         }
200     );
201     $record_processor->process($record);
202
203     # grab the XML, run it through our stylesheet, push it out to the browser
204     my $itemsxml;
205     if ( $xslsyspref eq "OPACXSLTDetailsDisplay" || $xslsyspref eq "XSLTDetailsDisplay" || $xslsyspref eq "XSLTResultsDisplay" ) {
206         $itemsxml = ""; #We don't use XSLT for items display on these pages
207     } else {
208         $itemsxml = buildKohaItemsNamespace($biblionumber, $hidden_items, $items_rs);
209     }
210     my $xmlrecord = $record->as_xml(C4::Context->preference('marcflavour'));
211
212     $variables ||= {};
213     my $biblio;
214     if ( $interface eq 'opac' && C4::Context->preference('OPACShowOpenURL')) {
215         my @biblio_itemtypes;
216         $biblio //= Koha::Biblios->find($biblionumber);
217         if (C4::Context->preference('item-level_itypes')) {
218             @biblio_itemtypes = $biblio->items->get_column("itype");
219         } else {
220             push @biblio_itemtypes, $biblio->itemtype;
221         }
222         my @itypes = split( /\s/, C4::Context->preference('OPACOpenURLItemTypes') );
223         my %original = ();
224         map { $original{$_} = 1 } @biblio_itemtypes;
225         if ( grep { $original{$_} } @itypes ) {
226             $variables->{OpenURLResolverURL} = $biblio->get_openurl;
227         }
228     }
229
230     # embed variables
231     my $varxml = "<variables>\n";
232     while (my ($key, $value) = each %$variables) {
233         $value //= q{};
234         $varxml .= "<variable name=\"$key\">$value</variable>\n";
235     }
236     $varxml .= "</variables>\n";
237
238     my $sysxml = get_xslt_sysprefs();
239     $xmlrecord =~ s/\<\/record\>/$itemsxml$sysxml$varxml\<\/record\>/;
240     if ($fixamps) { # We need to correct the ampersand entities that Zebra outputs
241         $xmlrecord =~ s/\&amp;amp;/\&amp;/g;
242         $xmlrecord =~ s/\&amp\;lt\;/\&lt\;/g;
243         $xmlrecord =~ s/\&amp\;gt\;/\&gt\;/g;
244     }
245     $xmlrecord =~ s/\& /\&amp\; /;
246     $xmlrecord =~ s/\&amp\;amp\; /\&amp\; /;
247
248     #If the xslt should fail, we will return undef (old behavior was
249     #raw MARC)
250     #Note that we did set do_not_return_source at object construction
251     return $engine->transform($xmlrecord, $xslfilename ); #file or URL
252 }
253
254 =head2 buildKohaItemsNamespace
255
256     my $items_xml = buildKohaItemsNamespace( $biblionumber, [ $hidden_items, $items ] );
257
258 Returns XML for items. It accepts two optional parameters:
259 - I<$hidden_items>: An arrayref of itemnumber values, for items that should be hidden
260 - I<$items>: A Koha::Items resultset, for the items to be returned
261
262 If both parameters are passed, I<$items> is used as the basis resultset, and I<$hidden_items>
263 are filtered out of it.
264
265 Is only used in this module currently.
266
267 =cut
268
269 sub buildKohaItemsNamespace {
270     my ($biblionumber, $hidden_items, $items_rs) = @_;
271
272     $hidden_items ||= [];
273
274     my $query = {};
275     $query = { 'me.itemnumber' => { not_in => $hidden_items } }
276       if $hidden_items;
277
278     unless ( $items_rs && ref($items_rs) eq 'Koha::Items' ) {
279         $query->{'me.biblionumber'} = $biblionumber;
280         $items_rs = Koha::Items->new;
281     }
282
283     my $items = $items_rs->search( $query, { prefetch => [ 'branchtransfers', 'reserves' ] } );
284
285     my $shelflocations =
286       { map { $_->{authorised_value} => $_->{opac_description} } Koha::AuthorisedValues->get_descriptions_by_koha_field( { frameworkcode => "", kohafield => 'items.location' } ) };
287     my $ccodes =
288       { map { $_->{authorised_value} => $_->{opac_description} } Koha::AuthorisedValues->get_descriptions_by_koha_field( { frameworkcode => "", kohafield => 'items.ccode' } ) };
289
290     my %branches = map { $_->branchcode => $_->branchname } Koha::Libraries->search({}, { order_by => 'branchname' })->as_list;
291
292     my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search->unblessed } };
293     my $xml = '';
294     my %descs = map { $_->{authorised_value} => $_ } Koha::AuthorisedValues->get_descriptions_by_koha_field( { kohafield => 'items.notforloan' } );
295     my $ref_status = C4::Context->preference('Reference_NFL_Statuses') // q{}; # fallback to 1|2 removed
296
297     while ( my $item = $items->next ) {
298         my $status;
299         my $substatus = '';
300         my $recalls_count;
301
302         if ( C4::Context->preference('UseRecalls') ) {
303             $recalls_count = Koha::Recalls->search({ item_id => $item->itemnumber, status => 'waiting' })->count;
304         }
305
306         if ($recalls_count) {
307             # recalls take priority over holds
308             $status = 'other';
309             $substatus = 'Recall waiting';
310         }
311         elsif ( $item->has_pending_hold ) {
312             $status = 'other';
313             $substatus = 'Pending hold';
314         }
315         elsif ( $item->holds->waiting->count ) {
316             $status = 'other';
317             $substatus = 'Hold waiting';
318         }
319         elsif ($item->get_transfer) {
320             $status = 'other';
321             $substatus = 'In transit';
322         }
323         elsif ($item->damaged) {
324             $status = 'other';
325             $substatus = "Damaged";
326         }
327         elsif ($item->itemlost) {
328             $status = 'other';
329             $substatus = "Lost";
330         }
331         elsif ( $item->withdrawn) {
332             $status = 'other';
333             $substatus = "Withdrawn";
334         }
335         elsif ($item->onloan) {
336             $status = 'other';
337             $substatus = "Checked out";
338         }
339         elsif ( $item->notforloan ) {
340             $status = $item->notforloan =~ /^($ref_status)$/
341                 ? "reference"
342                 : "reallynotforloan";
343             $substatus = exists $descs{$item->notforloan} ? $descs{$item->notforloan}->{opac_description} : "Not for loan";
344         }
345         elsif ( exists $itemtypes->{ $item->effective_itemtype }
346             && $itemtypes->{ $item->effective_itemtype }->{notforloan}
347             && $itemtypes->{ $item->effective_itemtype }->{notforloan} == 1 )
348         {
349             $status = "1" =~ /^($ref_status)$/
350                 ? "reference"
351                 : "reallynotforloan";
352             $substatus = "Not for loan";
353         }
354         else {
355             $status = "available";
356         }
357         my $homebranch     = C4::Koha::xml_escape($branches{$item->homebranch});
358         my $holdingbranch  = C4::Koha::xml_escape($branches{$item->holdingbranch});
359         my $resultbranch   = C4::Context->preference('OPACResultsLibrary') eq 'homebranch' ? $homebranch : $holdingbranch;
360         my $location       = C4::Koha::xml_escape($item->location && exists $shelflocations->{$item->location} ? $shelflocations->{$item->location} : $item->location);
361         my $ccode          = C4::Koha::xml_escape($item->ccode    && exists $ccodes->{$item->ccode}            ? $ccodes->{$item->ccode}            : $item->ccode);
362         my $itemcallnumber = C4::Koha::xml_escape($item->itemcallnumber);
363         my $stocknumber    = C4::Koha::xml_escape($item->stocknumber);
364         $xml .=
365             "<item>"
366           . "<homebranch>$homebranch</homebranch>"
367           . "<holdingbranch>$holdingbranch</holdingbranch>"
368           . "<resultbranch>$resultbranch</resultbranch>"
369           . "<location>$location</location>"
370           . "<ccode>$ccode</ccode>"
371           . "<status>".( $status // q{} )."</status>"
372           . "<substatus>$substatus</substatus>"
373           . "<itemcallnumber>$itemcallnumber</itemcallnumber>"
374           . "<stocknumber>$stocknumber</stocknumber>"
375           . "</item>";
376     }
377     $xml = "<items xmlns=\"http://www.koha-community.org/items\">".$xml."</items>";
378     return $xml;
379 }
380
381 =head2 engine
382
383 Returns reference to XSLT handler object.
384
385 =cut
386
387 sub engine {
388     return $engine;
389 }
390
391 1;
392
393 __END__
394
395 =head1 AUTHOR
396
397 Joshua Ferraro <jmf@liblime.com>
398
399 Koha Development Team <http://koha-community.org/>
400
401 =cut