Main Koha release repository https://koha-community.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

376 lines
9.7 KiB

package Koha::XSLT_Handler;
# Copyright 2014 Rijksmuseum
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
Koha::XSLT_Handler - Facilitate use of XSLT transformations
=head1 SYNOPSIS
use Koha::XSLT_Handler;
my $xslt_engine = Koha::XSLT_Handler->new;
my $output = $xslt_engine->transform($xml, $xsltfilename);
$output = $xslt_engine->transform({ xml => $xml, file => $file });
$output = $xslt_engine->transform({ xml => $xml, code => $code });
my $err= $xslt_engine->err; # error number
my $errstr= $xslt_engine->errstr; # error message
$xslt_engine->refresh($xsltfilename);
=head1 DESCRIPTION
A XSLT handler object on top of LibXML and LibXSLT, allowing you to
run XSLT stylesheets repeatedly without loading them again.
Errors occurring during loading, parsing or transforming are reported
via the err and errstr attributes.
Reloading XSLT files can be done with the refresh method.
=head1 METHODS
=head2 new
Create handler object (via Class::Accessor)
=head2 transform
Run transformation for specific string and stylesheet
=head2 refresh
Allow to reload stylesheets when transforming again
=head1 PROPERTIES
=head2 err
Error number (see list of ERROR CODES)
=head2 errstr
Error message
=head2 do_not_return_source
If true, transform returns undef on failure. By default, it returns the
original string passed. Errors are reported as described.
=head2 print_warns
If set, print error messages to STDERR. True by default.
=head1 ERROR CODES
=head2 Error 1
No XSLT file passed
=head2 Error 2
XSLT file not found
=head2 Error 3
Error while loading stylesheet xml: [furter information]
=head2 Error 4
Error while parsing stylesheet: [furter information]
=head2 Error 5
Error while parsing input: [furter information]
=head2 Error 6
Error while transforming input: [furter information]
=head2 Error 7
No string to transform
=head1 INTERNALS
For documentation purposes. You are not encouraged to access them.
=head2 last_xsltfile
Contains the last successfully executed XSLT filename
=head2 xslt_hash
Hash reference to loaded stylesheets
=head1 ADDITIONAL COMMENTS
=cut
use Modern::Perl;
use XML::LibXML;
use XML::LibXSLT;
use base qw(Class::Accessor);
__PACKAGE__->mk_ro_accessors(qw( err errstr ));
__PACKAGE__->mk_accessors(qw( do_not_return_source print_warns ));
=head2 transform
my $output= $xslt_engine->transform( $xml, $xsltfilename );
#Alternatively:
#$output = $xslt_engine->transform({ xml => $xml, file => $file, [parameters => $parameters] });
#$output = $xslt_engine->transform({ xml => $xml, code => $code, [parameters => $parameters] });
if( $xslt_engine->err ) {
#decide what to do on failure..
}
my $output2= $xslt_engine->transform( $xml2 );
Pass a xml string and a fully qualified path of a XSLT file.
Instead of a filename, you may also pass a URL.
You may also pass the contents of a xsl file as a string like $code above.
If you do not pass a filename, the last file used is assumed.
Returns the transformed string.
Check the error number in err to know if something went wrong.
In that case do_not_return_source did determine the return value.
=cut
sub transform {
my $self = shift;
#check parameters
# old style: $xml, $filename
# new style: $hashref
my ( $xml, $filename, $xsltcode );
my $parameters = {};
if( ref $_[0] eq 'HASH' ) {
$xml = $_[0]->{xml};
$xsltcode = $_[0]->{code};
$filename = $_[0]->{file} if !$xsltcode; #xsltcode gets priority
$parameters = $_[0]->{parameters} if ref $_[0]->{parameters} eq 'HASH';
} else {
( $xml, $filename ) = @_;
}
#Initialized yet?
if ( !$self->{xslt_hash} ) {
$self->_init;
}
else {
$self->_set_error; #clear last error
}
my $retval = $self->{do_not_return_source} ? undef : $xml;
#check if no string passed
if ( !defined $xml ) {
$self->_set_error(7);
return; #always undef
}
#load stylesheet
my $key = $self->_load( $filename, $xsltcode );
my $stsh = $key? $self->{xslt_hash}->{$key}: undef;
return $retval if $self->{err};
#parse input and transform
my $parser = XML::LibXML->new();
my $source = eval { $parser->parse_string($xml) };
if ($@) {
$self->_set_error( 5, $@ );
return $retval;
}
my $str = eval {
#$parameters is an optional hashref that contains
#key-value pairs to be sent to the XSLT.
#Numbers may be bare but strings must be double quoted
#(e.g. "'string'" or '"string"'). See XML::LibXSLT for
#more details.
#NOTE: Parameters are not cached. They are provided for
#each different transform.
my $result = $stsh->transform($source, %$parameters);
$stsh->output_as_chars($result);
};
if ($@) {
$self->_set_error( 6, $@ );
return $retval;
}
$self->{last_xsltfile} = $key;
return $str;
}
=head2 refresh
$xslt_engine->refresh;
$xslt_engine->refresh( $xsltfilename );
Pass a file for an individual refresh or no file to refresh all.
Refresh returns the number of items affected.
What we actually do, is just clear the internal cache for reloading next
time when transform is called.
The return value is mainly theoretical. Since this is supposed to work
always(...), there is no actual need to test it.
Note that refresh does also clear the error information.
=cut
sub refresh {
my ( $self, $file ) = @_;
$self->_set_error;
return if !$self->{xslt_hash};
my $rv;
if ($file) {
$rv = delete $self->{xslt_hash}->{$file} ? 1 : 0;
}
else {
$rv = scalar keys %{ $self->{xslt_hash} };
$self->{xslt_hash} = {};
}
return $rv;
}
# ************** INTERNAL ROUTINES ********************************************
# _init
# Internal routine for initialization.
sub _init {
my $self = shift;
$self->_set_error;
$self->{xslt_hash} = {};
$self->{print_warns} = 1 unless exists $self->{print_warns};
$self->{do_not_return_source} = 0
unless exists $self->{do_not_return_source};
#by default we return source on a failing transformation
#but it could be passed at construction time already
return;
}
# _load
# Internal routine for loading a new stylesheet.
sub _load {
my ( $self, $filename, $code ) = @_;
my ( $digest, $codelen, $salt, $rv );
$salt = 'AZ'; #just a constant actually
#If no file or code passed, use the last file again
if ( !$filename && !$code ) {
my $last = $self->{last_xsltfile};
if ( !$last || !exists $self->{xslt_hash}->{$last} ) {
$self->_set_error(1);
return;
}
return $last;
}
#check if it is loaded already
if( $code ) {
$codelen = length( $code );
$digest = eval { crypt($code, $salt) };
if( $digest && exists $self->{xslt_hash}->{$digest.$codelen} ) {
return $digest.$codelen;
}
} elsif( $filename && exists $self->{xslt_hash}->{$filename} ) {
return $filename;
}
#Check file existence (skipping URLs)
if( $filename && $filename !~ /^https?:\/\// && !-e $filename ) {
$self->_set_error(2);
return;
}
#load sheet
my $parser = XML::LibXML->new;
my $style_doc = eval {
$parser->load_xml( $self->_load_xml_args($filename, $code) )
};
if ($@) {
$self->_set_error( 3, $@ );
return;
}
#parse sheet
my $xslt = XML::LibXSLT->new;
$rv = $code? $digest.$codelen: $filename;
$self->{xslt_hash}->{$rv} = eval { $xslt->parse_stylesheet($style_doc) };
if ($@) {
$self->_set_error( 4, $@ );
delete $self->{xslt_hash}->{$rv};
return;
}
return $rv;
}
sub _load_xml_args {
my $self = shift;
return $_[1]? { 'string' => $_[1]//'' }: { 'location' => $_[0]//'' };
}
# _set_error
# Internal routine for handling error information.
sub _set_error {
my ( $self, $errno, $addmsg ) = @_;
if ( !$errno ) { #clear the error
$self->{err} = undef;
$self->{errstr} = undef;
return;
}
$self->{err} = $errno;
if ( $errno == 1 ) {
$self->{errstr} = "No XSLT file passed.";
}
elsif ( $errno == 2 ) {
$self->{errstr} = "XSLT file not found.";
}
elsif ( $errno == 3 ) {
$self->{errstr} = "Error while loading stylesheet xml:";
}
elsif ( $errno == 4 ) {
$self->{errstr} = "Error while parsing stylesheet:";
}
elsif ( $errno == 5 ) {
$self->{errstr} = "Error while parsing input:";
}
elsif ( $errno == 6 ) {
$self->{errstr} = "Error while transforming input:";
}
elsif ( $errno == 7 ) {
$self->{errstr} = "No string to transform.";
}
if ($addmsg) {
$self->{errstr} .= " $addmsg";
}
warn $self->{errstr} if $self->{print_warns};
return;
}
=head1 AUTHOR
Marcel de Rooy, Rijksmuseum Netherlands
=cut
1;