Files
2024-10-14 00:08:40 +02:00

380 lines
11 KiB
Perl

# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::GenericInterface::Mapping::XSLT;
use strict;
use warnings;
use Kernel::System::VariableCheck qw(:all);
use Storable;
our $ObjectManagerDisabled = 1;
=head1 NAME
Kernel::GenericInterface::Mapping::XSLT - GenericInterface C<XSLT> data mapping backend
=head1 PUBLIC INTERFACE
=head2 new()
usually, you want to create an instance of this
by using Kernel::GenericInterface::Mapping->new();
=cut
sub new {
my ( $Type, %Param ) = @_;
# Allocate new hash for object.
my $Self = {};
bless( $Self, $Type );
# Check needed params.
for my $Needed (qw(DebuggerObject MappingConfig)) {
if ( !$Param{$Needed} ) {
return {
Success => 0,
ErrorMessage => "Got no $Needed!"
};
}
$Self->{$Needed} = $Param{$Needed};
}
# Check mapping config.
if ( !IsHashRefWithData( $Param{MappingConfig} ) ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Got no MappingConfig as hash ref with content!',
);
}
# Check config - if we have a map config, it has to be a non-empty hash ref.
if (
defined $Param{MappingConfig}->{Config}
&& !IsHashRefWithData( $Param{MappingConfig}->{Config} )
)
{
return $Self->{DebuggerObject}->Error(
Summary => 'Got MappingConfig with Data, but Data is no hash ref with content!',
);
}
return $Self;
}
=head2 Map()
Provides mapping based on C<XSLT> style sheets.
Additional data is provided by the function results from various stages in requester and provider.
This data can be included in the C<XSLT> mapping as 'DataInclude' structure via configuration.
my $ReturnData = $MappingObject->Map(
Data => {
'original_key' => 'original_value',
'another_key' => 'next_value',
},
DataInclude => {
RequesterRequestInput => { ... },
RequesterRequestPrepareOutput => { ... },
RequesterRequestMapOutput => { ... },
RequesterResponseInput => { ... },
RequesterResponseMapOutput => { ... },
RequesterErrorHandlingOutput => { ... },
ProviderRequestInput => { ... },
ProviderRequestMapOutput => { ... },
ProviderResponseInput => { ... },
ProviderResponseMapOutput => { ... },
ProviderErrorHandlingOutput => { ... },
},
}
);
my $ReturnData = {
'changed_key' => 'changed_value',
'original_key' => 'another_changed_value',
'another_original_key' => 'default_value',
'default_key' => 'changed_value',
};
=cut
sub Map {
my ( $Self, %Param ) = @_;
# Check data - only accept undef or hash ref or array ref.
if ( defined $Param{Data} && ref $Param{Data} ne 'HASH' && ref $Param{Data} ne 'ARRAY' ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Got Data but it is not a hash or array ref in Mapping XSLT backend!'
);
}
# Check included data - only accept undef or hash ref.
if ( defined $Param{DataInclude} && !IsHashRefWithData( $Param{DataInclude} ) ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Got DataInclude but it is not a hash ref in Mapping XSLT backend!'
);
}
# Return if data is empty.
if ( !defined $Param{Data} || !%{ $Param{Data} } ) {
return {
Success => 1,
Data => {},
};
}
# Prepare short config variable.
my $Config = $Self->{MappingConfig}->{Config};
# No config means we just return input data.
if ( !$Config || !$Config->{Template} ) {
return {
Success => 1,
Data => $Param{Data},
};
}
# Load required libraries (XML::LibXML and XML::LibXSLT).
LIBREQUIRED:
for my $LibRequired (qw(XML::LibXML XML::LibXSLT)) {
my $LibFound = $Kernel::OM->Get('Kernel::System::Main')->Require($LibRequired);
next LIBREQUIRED if $LibFound;
return $Self->{DebuggerObject}->Error(
Summary => "Could not find required library $LibRequired",
);
}
# Prepare style sheet.
my $LibXSLT = XML::LibXSLT->new();
# Remove template line breaks and white spaces to plain text lines on the fly, see bug# 14106.
my $Template =
$Config->{Template}
=~ s{ > [ \t\n]+ (?= [^< \t\n] ) }{>}xmsgr
=~ s{ (?<! [> \t\n] ) [ \t\n]+ < }{<}xmsgr;
my ( $StyleDoc, $StyleSheet );
eval {
$StyleDoc = XML::LibXML->load_xml(
string => $Template,
no_cdata => 1,
);
};
if ( !$StyleDoc ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Could not load configured XSLT template',
Data => $Template,
);
}
eval {
$StyleSheet = $LibXSLT->parse_stylesheet($StyleDoc);
};
if ( !$StyleSheet ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Could not parse configured XSLT template',
Data => $@,
);
}
# Append the configured include data to the normal data structure.
if (
IsHashRefWithData( $Param{DataInclude} )
&& IsArrayRefWithData( $Config->{DataInclude} )
)
{
my $MergedData = Storable::dclone( $Param{Data} );
DATAINCLUDEMODULE:
for my $DataIncludeModule ( @{ $Config->{DataInclude} } ) {
next DATAINCLUDEMODULE if !$Param{DataInclude}->{$DataIncludeModule};
# Clone the data include hash to prevent circular data structure references
$MergedData->{DataInclude}->{$DataIncludeModule}
= Storable::dclone( $Param{DataInclude}->{$DataIncludeModule} );
}
$Self->{DebuggerObject}->Debug(
Summary => 'Data merged with DataInclude before mapping',
Data => $MergedData,
);
$Param{Data} = $MergedData;
}
# Note: XML::Simple was chosen over alternatives like XML::LibXML and XML::Dumper
# due to its simplicity and because we just require a straightforward conversion.
# Other modules provide more possibilities but don't allow directly exporting a complete
# and clean structure.
# Reference:
# http://www.perlmonks.org/?node_id=490846
# http://stackoverflow.com/questions/12182129/convert-string-to-hash-using-libxml-in-perl
# XSTL regex recursion.
if ( IsArrayRefWithData( $Config->{PreRegExFilter} ) ) {
$Self->_RegExRecursion(
Data => $Param{Data},
Config => $Config->{PreRegExFilter},
);
$Self->{DebuggerObject}->Debug(
Summary => 'Data before mapping after Pre RegExFilter',
Data => $Param{Data},
);
}
# Convert data to xml structure.
$Kernel::OM->Get('Kernel::System::Main')->Require('XML::Simple');
my $XMLSimple = XML::Simple->new();
my $XMLPre;
eval {
$XMLPre = $XMLSimple->XMLout(
$Param{Data},
AttrIndent => 1,
ContentKey => '-content',
NoAttr => 1,
KeyAttr => [],
RootName => 'RootElement',
);
};
if ( !$XMLPre ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Could not convert data from Perl to XML before mapping',
Data => $@,
);
}
# Transform xml data.
my ( $XMLSource, $Result );
eval {
$XMLSource = XML::LibXML->load_xml(
string => $XMLPre,
no_cdata => 1,
);
};
if ( !$XMLSource ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Could not load data after conversion from Perl to XML',
Data => $XMLPre,
);
}
eval {
$Result = $StyleSheet->transform($XMLSource);
};
if ( !$Result ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Could not map data',
Data => $@,
);
}
my $XMLPost = $StyleSheet->output_as_bytes($Result);
if ( !$XMLPost ) {
return $Self->{DebuggerObject}->Error(
Summary => "Could not write mapped data",
);
}
# Convert data back to perl structure.
my $ReturnData;
eval {
$ReturnData = $XMLSimple->XMLin(
$XMLPost,
ForceArray => 0,
ContentKey => '-content',
NoAttr => 1,
KeyAttr => [],
);
};
if ( !$ReturnData ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Could not convert data from XML to Perl after mapping',
Data => [ $@, $XMLPost ],
);
}
# XST regex recursion.
if ( IsArrayRefWithData( $Config->{PostRegExFilter} ) ) {
$Self->{DebuggerObject}->Debug(
Summary => 'Data after mapping before Post RegExFilter',
Data => $ReturnData,
);
$Self->_RegExRecursion(
Data => $ReturnData,
Config => $Config->{PostRegExFilter},
);
}
return {
Success => 1,
Data => $ReturnData,
};
}
sub _RegExRecursion {
my ( $Self, %Param ) = @_;
# Data must be hash ref.
return 1 if !IsHashRefWithData( $Param{Data} );
KEY:
for my $Key ( sort keys %{ $Param{Data} } ) {
# Recurse for array data in value.
if ( IsArrayRefWithData( $Param{Data}->{$Key} ) ) {
ARRAYENTRY:
for my $ArrayEntry ( @{ $Param{Data}->{$Key} } ) {
$Self->_RegExRecursion(
Data => $ArrayEntry,
Config => $Param{Config},
);
}
}
# Recurse directly otherwise (assume hash reference).
else {
$Self->_RegExRecursion(
Data => $Param{Data}->{$Key},
Config => $Param{Config},
);
}
# Apply configured RegEx to key.
REGEX:
for my $RegEx ( @{ $Param{Config} } ) {
next REGEX if $Key !~ m{$RegEx->{Search}};
# Double-eval the replacement string to allow embedded capturing group replacements,
# e.g. Search = '(foo|bar)bar', Replace = '$1foo'
# turns 'foobar' into 'foofoo' and 'barbar' into 'barfoo'.
my $NewKey = $Key =~ s{$RegEx->{Search}}{ '"' . $RegEx->{Replace} . '"' }eer;
# Skip if new key is equivalent to old key.
next KEY if $NewKey eq $Key;
# Replace matched key with new one.
$Param{Data}->{$NewKey} = delete $Param{Data}->{$Key};
}
}
return 1;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<https://otrs.org/>).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut