1 package Koha::XSLT::Base;
3 # Copyright 2014, 2019 Rijksmuseum, Prosentient Systems
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 Koha::XSLT::Base - Facilitate use of XSLT transformations
27 my $xslt_engine = Koha::XSLT::Base->new;
28 my $output = $xslt_engine->transform($xml, $xsltfilename);
29 $output = $xslt_engine->transform({ xml => $xml, file => $file });
30 $output = $xslt_engine->transform({ xml => $xml, code => $code });
31 my $err= $xslt_engine->err; # error code
32 $xslt_engine->refresh($xsltfilename);
36 A XSLT handler object on top of LibXML and LibXSLT, allowing you to
37 run XSLT stylesheets repeatedly without loading them again.
38 Errors occurring during loading, parsing or transforming are reported
39 via the err attribute.
40 Reloading XSLT files can be done with the refresh method.
42 The module refers to a (temporary) helper module Koha::XSLT::HTTPS that
43 resolves issues in libxml2/libxslt for https references.
53 Run transformation for specific string and stylesheet
57 Allow to reload stylesheets when transforming again
63 Error code (see list of ERROR CODES)
65 =head2 do_not_return_source
67 If true, transform returns undef on failure. By default, it returns the
68 original string passed. Errors are reported as described.
72 If set, print error messages to STDERR. False by default.
76 =head2 Error XSLTH_ERR_NO_FILE
80 =head2 Error XSLTH_ERR_FILE_NOT_FOUND
84 =head2 Error XSLTH_ERR_LOADING
86 Error while loading stylesheet xml: [optional warnings]
88 =head2 Error XSLTH_ERR_PARSING_CODE
90 Error while parsing stylesheet: [optional warnings]
92 =head2 Error XSLTH_ERR_PARSING_DATA
94 Error while parsing input: [optional warnings]
96 =head2 Error XSLTH_ERR_TRANSFORMING
98 Error while transforming input: [optional warnings]
100 =head2 Error XSLTH_NO_STRING_PASSED
102 No string to transform
106 For documentation purposes. You are not encouraged to access them.
110 Contains the last successfully executed XSLT filename
114 Hash reference to loaded stylesheets
116 =head1 ADDITIONAL COMMENTS
123 use Koha::XSLT::HTTPS;
124 use Koha::XSLT::Security;
126 use base qw(Class::Accessor);
128 __PACKAGE__->mk_ro_accessors(qw( err ));
129 __PACKAGE__->mk_accessors(qw( do_not_return_source print_warns ));
131 use constant XSLTH_ERR_1 => 'XSLTH_ERR_NO_FILE';
132 use constant XSLTH_ERR_2 => 'XSLTH_ERR_FILE_NOT_FOUND';
133 use constant XSLTH_ERR_3 => 'XSLTH_ERR_LOADING';
134 use constant XSLTH_ERR_4 => 'XSLTH_ERR_PARSING_CODE';
135 use constant XSLTH_ERR_5 => 'XSLTH_ERR_PARSING_DATA';
136 use constant XSLTH_ERR_6 => 'XSLTH_ERR_TRANSFORMING';
137 use constant XSLTH_ERR_7 => 'XSLTH_NO_STRING_PASSED';
141 my $xslt_engine = Koha::XSLT::Base->new;
146 my ($class, $params) = @_;
147 my $self = $class->SUPER::new($params);
148 $self->{_security} = Koha::XSLT::Security->new;
149 $self->{_security}->register_callbacks;
155 my $output= $xslt_engine->transform( $xml, $xsltfilename, [$format] );
157 #$output = $xslt_engine->transform({ xml => $xml, file => $file, [parameters => $parameters], [format => ['chars'|'bytes'|'xmldoc']] });
158 #$output = $xslt_engine->transform({ xml => $xml, code => $code, [parameters => $parameters], [format => ['chars'|'bytes'|'xmldoc']] });
159 if( $xslt_engine->err ) {
160 #decide what to do on failure..
162 my $output2= $xslt_engine->transform( $xml2 );
164 Pass a xml string and a fully qualified path of a XSLT file.
165 Instead of a filename, you may also pass a URL.
166 You may also pass the contents of a xsl file as a string like $code above.
167 If you do not pass a filename, the last file used is assumed.
168 Normally returns the transformed string; if you pass format => 'xmldoc' in
169 the hash format, it returns a xml document object.
170 Check the error number in err to know if something went wrong.
171 In that case do_not_return_source did determine the return value.
179 # old style: $xml, $filename, $format
180 # new style: $hashref
181 my ( $xml, $filename, $xsltcode, $format );
183 if( ref $_[0] eq 'HASH' ) {
185 $xsltcode = $_[0]->{code};
186 $filename = $_[0]->{file} if !$xsltcode; #xsltcode gets priority
187 $parameters = $_[0]->{parameters} if ref $_[0]->{parameters} eq 'HASH';
188 $format = $_[0]->{format} || 'chars';
190 ( $xml, $filename, $format ) = @_;
195 if ( !$self->{xslt_hash} ) {
199 $self->_set_error; #clear last error
201 my $retval = $self->{do_not_return_source} ? undef : $xml;
203 #check if no string passed
204 if ( !defined $xml ) {
205 $self->_set_error( XSLTH_ERR_7 );
206 return; #always undef
210 my $key = $self->_load( $filename, $xsltcode );
211 my $stsh = $key? $self->{xslt_hash}->{$key}: undef;
212 return $retval if $self->{err};
214 #parse input and transform
215 my $parser = XML::LibXML->new();
216 $self->{_security}->set_parser_options($parser);
217 my $source = eval { $parser->parse_string($xml) };
219 $self->_set_error( XSLTH_ERR_5, $@ );
223 #$parameters is an optional hashref that contains
224 #key-value pairs to be sent to the XSLT.
225 #Numbers may be bare but strings must be double quoted
226 #(e.g. "'string'" or '"string"'). See XML::LibXSLT for
229 #NOTE: Parameters are not cached. They are provided for
230 #each different transform.
231 my $transformed = $stsh->transform($source, %$parameters);
233 ? $stsh->output_as_bytes( $transformed )
234 : $format eq 'xmldoc'
236 : $stsh->output_as_chars( $transformed ); # default: chars
239 $self->_set_error( XSLTH_ERR_6, $@ );
242 $self->{last_xsltfile} = $key;
248 $xslt_engine->refresh;
249 $xslt_engine->refresh( $xsltfilename );
251 Pass a file for an individual refresh or no file to refresh all.
252 Refresh returns the number of items affected.
253 What we actually do, is just clear the internal cache for reloading next
254 time when transform is called.
255 The return value is mainly theoretical. Since this is supposed to work
256 always(...), there is no actual need to test it.
257 Note that refresh does also clear the error information.
262 my ( $self, $file ) = @_;
264 return if !$self->{xslt_hash};
267 $rv = delete $self->{xslt_hash}->{$file} ? 1 : 0;
270 $rv = scalar keys %{ $self->{xslt_hash} };
271 $self->{xslt_hash} = {};
276 # ************** INTERNAL ROUTINES ********************************************
279 # Internal routine for initialization.
285 $self->{xslt_hash} = {};
286 $self->{print_warns} = 1 unless exists $self->{print_warns};
287 $self->{do_not_return_source} = 0
288 unless exists $self->{do_not_return_source};
290 #by default we return source on a failing transformation
291 #but it could be passed at construction time already
296 # Internal routine for loading a new stylesheet.
299 my ( $self, $filename, $code ) = @_;
300 my ( $digest, $codelen, $salt, $rv );
301 $salt = 'AZ'; #just a constant actually
303 #If no file or code passed, use the last file again
304 if ( !$filename && !$code ) {
305 my $last = $self->{last_xsltfile};
306 if ( !$last || !exists $self->{xslt_hash}->{$last} ) {
307 $self->_set_error( XSLTH_ERR_1 );
313 #check if it is loaded already
315 $codelen = length( $code );
316 $digest = eval { crypt($code, $salt) };
317 if( $digest && exists $self->{xslt_hash}->{$digest.$codelen} ) {
318 return $digest.$codelen;
320 } elsif( $filename && exists $self->{xslt_hash}->{$filename} ) {
324 #Check file existence (skipping URLs)
325 if( $filename && $filename !~ /^https?:\/\// && !-e $filename ) {
326 $self->_set_error( XSLTH_ERR_2 );
331 my $parser = XML::LibXML->new;
332 $self->{_security}->set_parser_options($parser);
333 my $style_doc = eval {
334 $parser->load_xml( $self->_load_xml_args($filename, $code) )
337 $self->_set_error( XSLTH_ERR_3, $@ );
342 my $xslt = XML::LibXSLT->new;
343 $self->{_security}->set_callbacks($xslt);
345 $rv = $code? $digest.$codelen: $filename;
346 $self->{xslt_hash}->{$rv} = eval { $xslt->parse_stylesheet($style_doc) };
348 $self->_set_error( XSLTH_ERR_4, $@ );
349 delete $self->{xslt_hash}->{$rv};
356 my ( $self, $filename, $code ) = @_;
357 return Koha::XSLT::HTTPS->load($filename) if $filename && $filename =~ /^https/i;
358 # Workaround for current problems with https location in libxml2/libxslt
359 # Returns response like { string => SOME_CODE }
360 return $code ? { string => $code } : { location => $filename };
364 # Internal routine for handling error information.
367 my ( $self, $errcode, $warn ) = @_;
369 $self->{err} = $errcode; #set or clear error
370 warn 'XSLT::Base: '. $warn if $warn && $self->{print_warns};
375 Marcel de Rooy, Rijksmuseum Netherlands
376 David Cook, Prosentient Systems