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.
50 Run transformation for specific string and stylesheet
54 Allow to reload stylesheets when transforming again
60 Error code (see list of ERROR CODES)
62 =head2 do_not_return_source
64 If true, transform returns undef on failure. By default, it returns the
65 original string passed. Errors are reported as described.
69 If set, print error messages to STDERR. False by default.
73 =head2 Error XSLTH_ERR_NO_FILE
77 =head2 Error XSLTH_ERR_FILE_NOT_FOUND
81 =head2 Error XSLTH_ERR_LOADING
83 Error while loading stylesheet xml: [optional warnings]
85 =head2 Error XSLTH_ERR_PARSING_CODE
87 Error while parsing stylesheet: [optional warnings]
89 =head2 Error XSLTH_ERR_PARSING_DATA
91 Error while parsing input: [optional warnings]
93 =head2 Error XSLTH_ERR_TRANSFORMING
95 Error while transforming input: [optional warnings]
97 =head2 Error XSLTH_NO_STRING_PASSED
99 No string to transform
103 For documentation purposes. You are not encouraged to access them.
107 Contains the last successfully executed XSLT filename
111 Hash reference to loaded stylesheets
113 =head1 ADDITIONAL COMMENTS
120 use Koha::XSLT::Security;
122 use base qw(Class::Accessor);
124 __PACKAGE__->mk_ro_accessors(qw( err ));
125 __PACKAGE__->mk_accessors(qw( do_not_return_source print_warns ));
127 use constant XSLTH_ERR_1 => 'XSLTH_ERR_NO_FILE';
128 use constant XSLTH_ERR_2 => 'XSLTH_ERR_FILE_NOT_FOUND';
129 use constant XSLTH_ERR_3 => 'XSLTH_ERR_LOADING';
130 use constant XSLTH_ERR_4 => 'XSLTH_ERR_PARSING_CODE';
131 use constant XSLTH_ERR_5 => 'XSLTH_ERR_PARSING_DATA';
132 use constant XSLTH_ERR_6 => 'XSLTH_ERR_TRANSFORMING';
133 use constant XSLTH_ERR_7 => 'XSLTH_NO_STRING_PASSED';
137 my $xslt_engine = Koha::XSLT::Base->new;
142 my ($class, $params) = @_;
143 my $self = $class->SUPER::new($params);
144 $self->{_security} = Koha::XSLT::Security->new;
145 $self->{_security}->register_callbacks;
151 my $output= $xslt_engine->transform( $xml, $xsltfilename, [$format] );
153 #$output = $xslt_engine->transform({ xml => $xml, file => $file, [parameters => $parameters], [format => ['chars'|'bytes'|'xmldoc']] });
154 #$output = $xslt_engine->transform({ xml => $xml, code => $code, [parameters => $parameters], [format => ['chars'|'bytes'|'xmldoc']] });
155 if( $xslt_engine->err ) {
156 #decide what to do on failure..
158 my $output2= $xslt_engine->transform( $xml2 );
160 Pass a xml string and a fully qualified path of a XSLT file.
161 Instead of a filename, you may also pass a URL.
162 You may also pass the contents of a xsl file as a string like $code above.
163 If you do not pass a filename, the last file used is assumed.
164 Normally returns the transformed string; if you pass format => 'xmldoc' in
165 the hash format, it returns a xml document object.
166 Check the error number in err to know if something went wrong.
167 In that case do_not_return_source did determine the return value.
175 # old style: $xml, $filename, $format
176 # new style: $hashref
177 my ( $xml, $filename, $xsltcode, $format );
179 if( ref $_[0] eq 'HASH' ) {
181 $xsltcode = $_[0]->{code};
182 $filename = $_[0]->{file} if !$xsltcode; #xsltcode gets priority
183 $parameters = $_[0]->{parameters} if ref $_[0]->{parameters} eq 'HASH';
184 $format = $_[0]->{format} || 'chars';
186 ( $xml, $filename, $format ) = @_;
191 if ( !$self->{xslt_hash} ) {
195 $self->_set_error; #clear last error
197 my $retval = $self->{do_not_return_source} ? undef : $xml;
199 #check if no string passed
200 if ( !defined $xml ) {
201 $self->_set_error( XSLTH_ERR_7 );
202 return; #always undef
206 my $key = $self->_load( $filename, $xsltcode );
207 my $stsh = $key? $self->{xslt_hash}->{$key}: undef;
208 return $retval if $self->{err};
210 #parse input and transform
211 my $parser = XML::LibXML->new();
212 $self->{_security}->set_parser_options($parser);
213 my $source = eval { $parser->parse_string($xml) };
215 $self->_set_error( XSLTH_ERR_5, $@ );
219 #$parameters is an optional hashref that contains
220 #key-value pairs to be sent to the XSLT.
221 #Numbers may be bare but strings must be double quoted
222 #(e.g. "'string'" or '"string"'). See XML::LibXSLT for
225 #NOTE: Parameters are not cached. They are provided for
226 #each different transform.
227 my $transformed = $stsh->transform($source, %$parameters);
229 ? $stsh->output_as_bytes( $transformed )
230 : $format eq 'xmldoc'
232 : $stsh->output_as_chars( $transformed ); # default: chars
235 $self->_set_error( XSLTH_ERR_6, $@ );
238 $self->{last_xsltfile} = $key;
244 $xslt_engine->refresh;
245 $xslt_engine->refresh( $xsltfilename );
247 Pass a file for an individual refresh or no file to refresh all.
248 Refresh returns the number of items affected.
249 What we actually do, is just clear the internal cache for reloading next
250 time when transform is called.
251 The return value is mainly theoretical. Since this is supposed to work
252 always(...), there is no actual need to test it.
253 Note that refresh does also clear the error information.
258 my ( $self, $file ) = @_;
260 return if !$self->{xslt_hash};
263 $rv = delete $self->{xslt_hash}->{$file} ? 1 : 0;
266 $rv = scalar keys %{ $self->{xslt_hash} };
267 $self->{xslt_hash} = {};
272 # ************** INTERNAL ROUTINES ********************************************
275 # Internal routine for initialization.
281 $self->{xslt_hash} = {};
282 $self->{print_warns} = 1 unless exists $self->{print_warns};
283 $self->{do_not_return_source} = 0
284 unless exists $self->{do_not_return_source};
286 #by default we return source on a failing transformation
287 #but it could be passed at construction time already
292 # Internal routine for loading a new stylesheet.
295 my ( $self, $filename, $code ) = @_;
296 my ( $digest, $codelen, $salt, $rv );
297 $salt = 'AZ'; #just a constant actually
299 #If no file or code passed, use the last file again
300 if ( !$filename && !$code ) {
301 my $last = $self->{last_xsltfile};
302 if ( !$last || !exists $self->{xslt_hash}->{$last} ) {
303 $self->_set_error( XSLTH_ERR_1 );
309 #check if it is loaded already
311 $codelen = length( $code );
312 $digest = eval { crypt($code, $salt) };
313 if( $digest && exists $self->{xslt_hash}->{$digest.$codelen} ) {
314 return $digest.$codelen;
316 } elsif( $filename && exists $self->{xslt_hash}->{$filename} ) {
320 #Check file existence (skipping URLs)
321 if( $filename && $filename !~ /^https?:\/\// && !-e $filename ) {
322 $self->_set_error( XSLTH_ERR_2 );
327 my $parser = XML::LibXML->new;
328 $self->{_security}->set_parser_options($parser);
329 my $style_doc = eval {
330 $parser->load_xml( $self->_load_xml_args($filename, $code) )
333 $self->_set_error( XSLTH_ERR_3, $@ );
338 my $xslt = XML::LibXSLT->new;
339 $self->{_security}->set_callbacks($xslt);
341 $rv = $code? $digest.$codelen: $filename;
342 $self->{xslt_hash}->{$rv} = eval { $xslt->parse_stylesheet($style_doc) };
344 $self->_set_error( XSLTH_ERR_4, $@ );
345 delete $self->{xslt_hash}->{$rv};
353 return $_[1]? { 'string' => $_[1]//'' }: { 'location' => $_[0]//'' };
357 # Internal routine for handling error information.
360 my ( $self, $errcode, $warn ) = @_;
362 $self->{err} = $errcode; #set or clear error
363 warn 'XSLT::Base: '. $warn if $warn && $self->{print_warns};
368 Marcel de Rooy, Rijksmuseum Netherlands
369 David Cook, Prosentient Systems