This commit is contained in:
2024-10-14 00:08:40 +02:00
parent dbfba56f66
commit 1462d52e13
4572 changed files with 2658864 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
# --
# 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::Output::Template::Document;
## no critic(Perl::Critic::Policy::OTRS::RequireCamelCase)
use strict;
use warnings;
use parent qw (Template::Document);
our $ObjectManagerDisabled = 1;
=head1 NAME
Kernel::Output::Template::Document - Template Toolkit document extension package
=head1 PUBLIC INTERFACE
=head2 process()
process this template document. This method is inherited from
Template::Document and used to perform some up-front initialization
and processing.
=cut
sub process {
my ( $Self, $Context ) = @_;
$Self->_InstallOTRSExtensions($Context);
$Self->_PrecalculateBlockStructure($Context);
$Self->_PrecalculateBlockHookSubscriptions($Context);
return $Self->SUPER::process($Context);
}
=begin Internal:
=head2 _InstallOTRSExtensions()
adds some OTRS specific extensions to Template::Toolkit.
=cut
sub _InstallOTRSExtensions {
my ( $Self, $Context ) = @_;
# Already installed, nothing to do.
return if $Context->stash()->get('OTRS');
#
# Load the OTRS plugin. This will register some filters and functions.
#
$Context->stash()->set( 'OTRS', $Context->plugin('OTRS') );
#
# The RenderBlock macro makes it possible to use the old dtl:block-Style block calls
# that are still used by OTRS with Template::Toolkit.
#
# The block data is passed to the template, and this macro processes it and calls the relevant
# blocks.
#
$Context->stash()->set(
'PerformRenderBlock',
sub {
my $output = '';
my ( %_tt_args, $_tt_params );
$_tt_args{'BlockName'} = shift;
$_tt_params = shift;
$_tt_params = {} if ref $_tt_params ne 'HASH';
$_tt_params = { %_tt_args, %$_tt_params };
my $stash = $Context->localise($_tt_params);
eval {
my $BlockName = $stash->get('BlockName');
my $ParentBlock = $stash->get('ParentBlock') || $stash->{_BlockTree};
return if !exists $ParentBlock->{Children};
return if !exists $ParentBlock->{Children}->{$BlockName};
my $TemplateName = $stash->get('template')->{name} // '';
$TemplateName = substr( $TemplateName, 0, -3 ); # remove .tt extension
my $GenerateBlockHook =
$Context->{LayoutObject}->{_BlockHookSubscriptions}->{$TemplateName}->{$BlockName};
for my $TargetBlock ( @{ $ParentBlock->{Children}->{$BlockName} } ) {
$output .= "<!--HookStart${BlockName}-->\n" if $GenerateBlockHook;
$output .= $Context->process(
$TargetBlock->{Path},
{
'Data' => $TargetBlock->{Data},
'ParentBlock' => $TargetBlock,
},
);
$output .= "<!--HookEnd${BlockName}-->\n" if $GenerateBlockHook;
}
delete $ParentBlock->{Children}->{$BlockName};
};
$stash = $Context->delocalise();
die $@ if $@;
return $output;
}
);
#
# This block is used to cut out JavaScript code from the templates and insert it in the
# footer of the page, all in one place.
#
# Usage:
# [% WRAPPER JSOnDocumentComplete -%]
# console.log();
# [%- END %]
#
$Self->{_DEFBLOCKS}->{JSOnDocumentComplete} //= sub {
my $context = shift || die "template sub called without context\n";
my $stash = $context->stash();
my $output = '';
my $_tt_error;
eval {
if ( $stash->get( [ 'global', 0, 'KeepScriptTags', 0 ] ) ) {
$output .= $stash->get('content');
}
else {
push @{ $context->{LayoutObject}->{_JSOnDocumentComplete} }, $stash->get('content');
}
};
if ($@) {
$_tt_error = $context->catch( $@, \$output );
die $_tt_error if $_tt_error->type() ne 'return';
}
return $output;
};
#
# This block is used to insert the collected JavaScript code in the page footer.
#
$Self->{_DEFBLOCKS}->{JSOnDocumentCompleteInsert} //= sub {
my $context = shift || die "template sub called without context\n";
my $stash = $context->stash();
my $output = '';
my $_tt_error;
eval {
my $Code = join "\n", @{ $context->{LayoutObject}->{_JSOnDocumentComplete} || [] };
# remove opening script tags
$Code =~ s{ \s* <script[^>]+> (?:\s*<!--)? (?:\s*//\s*<!\[CDATA\[)? \n? }{}smxg;
# remove closing script tags
$Code =~ s{ \s* (?:-->\s*)? (?://\s*\]\]>\s*)? </script> \n? }{\n\n}smxg;
# remove additional newlines at the end of the code block
$Code =~ s{ \n+ \z }{\n}smxg;
$output .= $Code;
delete $context->{LayoutObject}->{_JSOnDocumentComplete};
};
if ($@) {
$_tt_error = $context->catch( $@, \$output );
die $_tt_error if $_tt_error->type() ne 'return';
}
return $output;
};
#
# This block is used to cut out JavaScript data that needs to be inserted to Core.Config
# from the templates and insert it in the footer of the page, all in one place.
#
# Usage:
# [% Process JSData
# Key = 'Test.Key'
# Value = { ... }
# %]
#
#
$Self->{_DEFBLOCKS}->{JSData} //= sub {
my $context = shift || die "template sub called without context\n";
my $stash = $context->stash();
my $output = '';
my $_tt_error;
eval {
my $Key = $stash->get('Key');
my $Value = $stash->get('Value');
return $output if !$Key;
$context->{LayoutObject}->{_JSData} //= {};
$context->{LayoutObject}->{_JSData}->{$Key} = $Value;
};
if ($@) {
$_tt_error = $context->catch( $@, \$output );
die $_tt_error if $_tt_error->type() ne 'return';
}
return $output;
};
#
# This block is used to insert the collected JavaScript data in the page footer.
#
$Self->{_DEFBLOCKS}->{JSDataInsert} //= sub {
my $context = shift || die "template sub called without context\n";
my $stash = $context->stash();
my $output = '';
my $_tt_error;
eval {
my %Data = %{ $context->{LayoutObject}->{_JSData} // {} };
if (%Data) {
my $JSONString = $Kernel::OM->Get('Kernel::System::JSON')->Encode(
Data => \%Data,
SortKeys => 1,
);
# Escape closing script tags in the JSON content as they will confuse the
# browser's parser.
$JSONString =~ s{ </(?<ScriptTag>script)}{<\\/$+{ScriptTag}}ismxg;
$output .= "Core.Config.AddConfig($JSONString);\n";
}
delete $context->{LayoutObject}->{_JSData};
};
if ($@) {
$_tt_error = $context->catch( $@, \$output );
die $_tt_error if $_tt_error->type() ne 'return';
}
return $output;
};
return;
}
=head2 _PrecalculateBlockStructure()
pre-calculates the tree structure of the blocks so that it
can be used by PerformRenderBlock in an efficient way.
=cut
sub _PrecalculateBlockStructure {
my ( $Self, $Context ) = @_;
my $Defblocks = $Self->{_DEFBLOCKS} || {};
my $BlockData = $Context->stash()->get( [ 'global', 0, 'BlockData', 0 ] ) || [];
return if !@{$BlockData};
my $BlockParents = {};
my $BlockPaths = {};
BLOCKPATHIDENTIFIER:
for my $BlockIdentifier ( sort keys %{$Defblocks} ) {
my @BlockPath = split( m{/}, $BlockIdentifier );
my $BlockPathLength = scalar @BlockPath;
next BLOCKPATHIDENTIFIER if !$BlockPathLength;
$BlockPaths->{ $BlockPath[-1] } = $BlockIdentifier;
$BlockParents->{ $BlockPath[-1] } = [ splice( @BlockPath, 0, $BlockPathLength - 1 ) ];
}
$Context->stash()->{_BlockTree} = {};
my $BlockIndex = 0;
BLOCK:
while ( $BlockIndex < @{$BlockData} ) {
my $Block = $BlockData->[$BlockIndex];
my $BlockName = $Block->{Name};
if ( !exists $BlockPaths->{$BlockName} ) {
$BlockIndex++;
next BLOCK;
}
my $BlockPointer = $Context->stash()->{_BlockTree};
PARENTBLOCK:
for my $ParentBlock ( @{ $BlockParents->{$BlockName} // [] } ) {
# Check if a parent node can be found
if (
!exists $BlockPointer->{Children}
|| !exists $BlockPointer->{Children}->{$ParentBlock}
)
{
# Parent node was not found. That means we dan discard this block.
splice @{$BlockData}, $BlockIndex, 1;
next BLOCK;
}
# Ok, parent block found, update block pointer to last element
$BlockPointer = $BlockPointer->{Children}->{$ParentBlock}->[-1];
}
$Block->{Path} = $BlockPaths->{$BlockName};
# Ok, the parent block pointer was apparently set correctly.
# Now append the data of our current block.
push @{ $BlockPointer->{Children}->{$BlockName} }, $Block;
# Remove block data
splice @{$BlockData}, $BlockIndex, 1;
}
return;
}
=head2 _PrecalculateBlockHookSubscriptions()
=cut
sub _PrecalculateBlockHookSubscriptions {
my ( $Self, $Context ) = @_;
# Only calculate once per LayoutObject
return if defined $Context->{LayoutObject}->{_BlockHookSubscriptions};
my $Config = $Kernel::OM->Get('Kernel::Config')->Get('Frontend::Template::GenerateBlockHooks') // {};
my %BlockHooks;
for my $Key ( sort keys %{ $Config // {} } ) {
for my $Template ( sort keys %{ $Config->{$Key} // {} } ) {
for my $Block ( @{ $Config->{$Key}->{$Template} // [] } ) {
$BlockHooks{$Template}->{$Block} = 1;
}
}
}
$Context->{LayoutObject}->{_BlockHookSubscriptions} = \%BlockHooks;
return;
}
1;
=end Internal:
=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

View File

@@ -0,0 +1,212 @@
# --
# 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::Output::Template::Plugin::OTRS;
use strict;
use warnings;
use parent qw(Template::Plugin);
use Scalar::Util;
our $ObjectManagerDisabled = 1;
=head1 NAME
Kernel::Output::Template::Plugin::OTRS - Template Toolkit extension plugin
=head1 PUBLIC INTERFACE
=head2 new()
this plugin registers a few filters and functions in Template::Toolkit.
These extensions have names starting with an uppercase letter so that
you can distinguish them from the builtins of Template::Toolkit which
are always lowercase.
Filters:
[% Data.MyData | Translate %] - Translate to user language.
[% Data.Created | Localize("TimeLong") %] - Format DateTime string according to user's locale.
[% Data.Created | Localize("TimeShort") %] - Format DateTime string according to user's locale, without seconds.
[% Data.Created | Localize("Date") %] - Format DateTime string according to user's locale, only date.
[% Data.Complex | Interpolate %] - Treat Data.Complex as a TT template and parse it.
[% Data.String | ReplacePlaceholders("one", "two") %] - Replace Data.String placeholders (i.e. %s) with supplied strings.
[% Data.Complex | JSON %] - Convert Data.Complex into a JSON string.
Functions:
[% Translate("Test string for %s", "Documentation") %] - Translate text, with placeholders.
[% Config("Home") %] - Get SysConfig configuration value.
[% Env("Baselink") %] - Get environment value of LayoutObject.
[% ReplacePlaceholders("This is %s", "<strong>bold text</strong>") %] - Replace string placeholders with supplied values.
=cut
sub new {
my ( $Class, $Context, @Params ) = @_;
# Produce a weak reference to the LayoutObject and use that in the filters.
# We do this because there could be more than one LayoutObject in the process,
# so we don't fetch it from the ObjectManager.
#
# Don't use $Context in the filters as that creates a circular dependency.
my $LayoutObject = $Context->{LayoutObject};
Scalar::Util::weaken($LayoutObject);
my $ConfigFunction = sub {
return $Kernel::OM->Get('Kernel::Config')->Get(@_);
};
my $EnvFunction = sub {
return $LayoutObject->{EnvRef}->{ $_[0] };
};
my $TranslateFunction = sub {
return $LayoutObject->{LanguageObject}->Translate(@_);
};
my $TranslateFilterFactory = sub {
my ( $FilterContext, @Parameters ) = @_;
return sub {
$LayoutObject->{LanguageObject}->Translate( $_[0], @Parameters );
};
};
my $LocalizeFunction = sub {
my $Format = $_[1];
if ( $Format eq 'TimeLong' ) {
return $LayoutObject->{LanguageObject}->FormatTimeString( $_[0], 'DateFormat' );
}
elsif ( $Format eq 'TimeShort' ) {
return $LayoutObject->{LanguageObject}->FormatTimeString( $_[0], 'DateFormat', 'NoSeconds' );
}
elsif ( $Format eq 'Date' ) {
return $LayoutObject->{LanguageObject}->FormatTimeString( $_[0], 'DateFormatShort' );
}
elsif ( $Format eq 'Filesize' ) {
return $LayoutObject->HumanReadableDataSize( Size => $_[0] );
}
return;
};
my $LocalizeFilterFactory = sub {
my ( $FilterContext, @Parameters ) = @_;
my $Format = $Parameters[0] || 'TimeLong';
return sub {
if ( $Format eq 'TimeLong' ) {
return $LayoutObject->{LanguageObject}->FormatTimeString( $_[0], 'DateFormat' );
}
elsif ( $Format eq 'TimeShort' ) {
return $LayoutObject->{LanguageObject}->FormatTimeString( $_[0], 'DateFormat', 'NoSeconds' );
}
elsif ( $Format eq 'Date' ) {
return $LayoutObject->{LanguageObject}->FormatTimeString( $_[0], 'DateFormatShort' );
}
elsif ( $Format eq 'Filesize' ) {
return $LayoutObject->HumanReadableDataSize( Size => $_[0] );
}
return;
};
};
# This filter processes the data as a template and replaces any contained TT tags.
# This is expensive and potentially dangerous, use with caution!
my $InterpolateFunction = sub {
# Don't parse if there are no TT tags present!
if ( index( $_[0], '[%' ) == -1 ) {
return $_[0];
}
return $Context->include( \$_[0] );
};
my $InterpolateFilterFactory = sub {
my ( $FilterContext, @Parameters ) = @_;
return sub {
# Don't parse if there are no TT tags present!
if ( index( $_[0], '[%' ) == -1 ) {
return $_[0];
}
return $FilterContext->include( \$_[0] );
};
};
# This filter replaces any placeholder occurrences in first parameter (i.e. %s or %d), with following parameters.
my $ReplacePlaceholdersFunction = sub {
my ( $Text, @Parameters ) = @_;
$Text //= '';
return $Text if !@Parameters;
for ( 0 .. $#Parameters ) {
return $Text if !defined $Parameters[$_];
$Text =~ s/%(s|d)/$Parameters[$_]/;
}
return $Text;
};
my $ReplacePlaceholdersFilter = sub {
my ( $FilterContext, @Parameters ) = @_;
return sub {
return $ReplacePlaceholdersFunction->( $_[0], @Parameters );
};
};
my $JSONFunction = sub {
return $LayoutObject->JSONEncode( Data => $_[0] );
};
my $JSONFilter = sub {
return $LayoutObject->JSONEncode( Data => $_[0] );
};
$Context->stash()->set( 'Config', $ConfigFunction );
$Context->stash()->set( 'Env', $EnvFunction );
$Context->stash()->set( 'Translate', $TranslateFunction );
$Context->stash()->set( 'Localize', $LocalizeFunction );
$Context->stash()->set( 'Interpolate', $InterpolateFunction );
$Context->stash()->set( 'ReplacePlaceholders', $ReplacePlaceholdersFunction );
$Context->stash()->set( 'JSON', $JSONFunction );
$Context->define_filter( 'Translate', [ $TranslateFilterFactory, 1 ] );
$Context->define_filter( 'Localize', [ $LocalizeFilterFactory, 1 ] );
$Context->define_filter( 'Interpolate', [ $InterpolateFilterFactory, 1 ] );
$Context->define_filter( 'ReplacePlaceholders', [ $ReplacePlaceholdersFilter, 1 ] );
$Context->define_filter( 'JSON', $JSONFilter );
return bless {
_CONTEXT => $Context,
_PARAMS => \@Params,
}, $Class;
}
=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
1;

View File

@@ -0,0 +1,706 @@
# --
# 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::Output::Template::Provider;
## no critic(Perl::Critic::Policy::OTRS::RequireCamelCase)
## nofilter(TidyAll::Plugin::OTRS::Perl::SyntaxCheck)
use strict;
use warnings;
use parent qw (Template::Provider);
use Scalar::Util qw();
use Template::Constants;
use Kernel::Output::Template::Document;
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Cache',
'Kernel::System::Encode',
'Kernel::System::Log',
'Kernel::System::Main',
);
# Force the use of our own document class.
$Template::Provider::DOCUMENT = 'Kernel::Output::Template::Document';
=head1 NAME
Kernel::Output::Template::Provider - Template Toolkit custom provider
=head1 PUBLIC INTERFACE
=head2 OTRSInit()
performs some post-initialization and creates a bridge between Template::Toolkit
and OTRS by adding the OTRS objects to the Provider object. This method must be
called after instantiating the Provider object.
Please note that we only store a weak reference to the LayoutObject to avoid ring
references.
=cut
sub OTRSInit {
my ( $Self, %Param ) = @_;
# Don't fetch LayoutObject via ObjectManager as there might be several instances involved
# at this point (for example in LinkObject there is an own LayoutObject to avoid block
# name collisions).
$Self->{LayoutObject} = $Param{LayoutObject} || die "Got no LayoutObject!";
#
# Store a weak reference to the LayoutObject to avoid ring references.
# We need it for the filters.
#
Scalar::Util::weaken( $Self->{LayoutObject} );
# define cache type
$Self->{CacheType} = 'TemplateProvider';
# caching can be disabled for debugging reasons
$Self->{CachingEnabled} = $Kernel::OM->Get('Kernel::Config')->Get('Frontend::TemplateCache') // 1;
return;
}
=begin Internal:
=head2 _fetch()
try to get a compiled version of a template from the CacheObject,
otherwise compile the template and return it.
Copied and slightly adapted from Template::Provider.
A note about caching: we have three levels of caching.
1. we have an in-memory cache that stores the compiled Document objects (fastest).
2. we store the parsed data in the CacheObject to be re-used in another request.
3. for string templates, we have an in-memory cache in the parsing method _compile().
It will return the already parsed object if it sees the same template content again.
=cut
sub _fetch {
my ( $self, $name, $t_name ) = @_;
my $stat_ttl = $self->{STAT_TTL};
$self->debug("_fetch($name)") if $self->{DEBUG};
# Check in-memory template cache if we already had this template.
$self->{_TemplateCache} //= {};
if ( $self->{_TemplateCache}->{$name} ) {
return $self->{_TemplateCache}->{$name};
}
# See if we already know the template is not found
if ( $self->{NOTFOUND}->{$name} ) {
return ( undef, Template::Constants::STATUS_DECLINED );
}
# Check if the template exists, is cacheable and if a cached version exists.
if ( -e $name && $self->{CachingEnabled} ) {
my $UserTheme = $self->{LayoutObject}->{EnvRef}->{UserTheme};
my $template_mtime = $self->_template_modified($name);
my $CacheKey = $self->_compiled_filename($name) . '::' . $template_mtime . '::' . $UserTheme;
# Is there an up-to-date compiled version in the cache?
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $self->{CacheType},
Key => $CacheKey,
);
if ( ref $Cache ) {
my $compiled_template = $Template::Provider::DOCUMENT->new($Cache);
# Store in-memory and return the compiled template
if ($compiled_template) {
# Make sure template cache does not get too big
if ( keys %{ $self->{_TemplateCache} } > 1000 ) {
$self->{_TemplateCache} = {};
}
$self->{_TemplateCache}->{$name} = $compiled_template;
return $compiled_template;
}
# Problem loading compiled template: warn and continue to fetch source template
warn( $self->error(), "\n" );
}
}
# load template from source
my ( $template, $error ) = $self->_load( $name, $t_name );
if ($error) {
# Template could not be fetched. Add to the negative/notfound cache.
$self->{NOTFOUND}->{$name} = time;
return ( $template, $error );
}
# compile template source
( $template, $error ) = $self->_compile( $template, $self->_compiled_filename($name) );
if ($error) {
# return any compile time error
return ( $template, $error );
}
# Make sure template cache does not get too big
if ( keys %{ $self->{_TemplateCache} } > 1000 ) {
$self->{_TemplateCache} = {};
}
$self->{_TemplateCache}->{$name} = $template->{data};
return $template->{data};
}
=head2 _load()
calls our pre processor when loading a template.
Inherited from Template::Provider.
=cut
sub _load {
my ( $Self, $Name, $Alias ) = @_;
my @Result = $Self->SUPER::_load( $Name, $Alias );
# If there was no error, pre-process our template
if ( ref $Result[0] ) {
$Result[0]->{text} = $Self->_PreProcessTemplateContent(
Content => $Result[0]->{text},
TemplateFile => $Result[0]->{name},
);
}
return @Result;
}
=head2 _compile()
compiles a .tt template into a Perl package and uses the CacheObject
to cache it.
Copied and slightly adapted from Template::Provider.
=cut
sub _compile {
my ( $self, $data, $compfile ) = @_;
my $text = $data->{text};
my ( $parsedoc, $error );
if ( $self->{DEBUG} ) {
$self->debug(
"_compile($data, ",
defined $compfile ? $compfile : '<no compfile>', ')'
);
}
# Check in-memory parser cache if we already had this template content
$self->{_ParserCache} //= {};
if ( $self->{_ParserCache}->{$text} ) {
return $self->{_ParserCache}->{$text};
}
my $parser = $self->{PARSER}
||= Template::Config->parser( $self->{PARAMS} )
|| return ( Template::Config->error(), Template::Constants::STATUS_ERROR );
# discard the template text - we don't need it any more
delete $data->{text};
# call parser to compile template into Perl code
if ( $parsedoc = $parser->parse( $text, $data ) ) {
$parsedoc->{METADATA} = {
'name' => $data->{name},
'modtime' => $data->{time},
%{ $parsedoc->{METADATA} },
};
# write the Perl code to the file $compfile, if defined
if ($compfile) {
my $UserTheme = $self->{LayoutObject}->{EnvRef}->{UserTheme};
my $CacheKey = $compfile . '::' . $data->{time} . '::' . $UserTheme;
if ( $self->{CachingEnabled} ) {
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $self->{CacheType},
TTL => 60 * 60 * 24,
Key => $CacheKey,
Value => $parsedoc,
);
}
}
if ( $data->{data} = $Template::Provider::DOCUMENT->new($parsedoc) ) {
# Make sure parser cache does not get too big
if ( keys %{ $self->{_ParserCache} } > 1000 ) {
$self->{_ParserCache} = {};
}
$self->{_ParserCache}->{$text} = $data;
return $data;
}
$error = $Template::Document::ERROR;
}
else {
$error = Template::Exception->new( 'parse', "$data->{ name } " . $parser->error() );
}
# return STATUS_ERROR, or STATUS_DECLINED if we're being tolerant
return $self->{TOLERANT}
? ( undef, Template::Constants::STATUS_DECLINED )
: ( $error, Template::Constants::STATUS_ERROR );
}
=end Internal:
=head2 store()
inherited from Template::Provider. This function override just makes sure that the original
in-memory cache cannot be used.
=cut
sub store {
my ( $Self, $Name, $Data ) = @_;
return $Data; # no-op
}
=begin Internal:
=head2 _PreProcessTemplateContent()
this is our template pre processor.
It handles some OTRS specific tags like [% InsertTemplate("TemplateName.tt") %]
and also performs compile-time code injection (ChallengeToken element into forms).
Besides that, it also makes sure the template is treated as UTF8.
This is run at compile time. If a template is cached, this method does not have to be executed on it
any more.
=cut
sub _PreProcessTemplateContent {
my ( $Self, %Param ) = @_;
my $Content = $Param{Content};
# Make sure the template is treated as utf8.
$Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( \$Content );
my $TemplateFileWithoutTT = substr( $Param{TemplateFile}, 0, -3 );
#
# Include other templates into this one before parsing.
# [% IncludeTemplate("DatePicker.tt") %]
#
my ( $ReplaceCounter, $Replaced );
do {
$Replaced = $Content =~ s{
\[% -? \s* InsertTemplate \( \s* ['"]? (.*?) ['"]? \s* \) \s* -? %\]\n?
}{
# Load the template via the provider.
# We'll use SUPER::load here because we don't need the preprocessing twice.
my $TemplateContent = ($Self->SUPER::load($1))[0];
$Kernel::OM->Get('Kernel::System::Encode')->EncodeInput(\$TemplateContent);
# Remove commented lines already here because of problems when the InsertTemplate tag
# is not on the beginning of the line.
$TemplateContent =~ s/^#.*\n//gm;
$TemplateContent;
}esmxg;
} until ( !$Replaced || ++$ReplaceCounter > 100 );
#
# Remove DTL-style comments (lines starting with #)
#
$Content =~ s/^#.*\n//gm if !$ENV{TEMPLATE_KEEP_COMMENTS};
#
# Insert a BLOCK call into the template.
# [% RenderBlock('b1') %]...[% END %]
# becomes
# [% PerformRenderBlock('b1') %][% BLOCK 'b1' %]...[% END %]
# This is what we need: define the block and call it from the RenderBlock macro
# to render it based on available block data from the frontend modules.
#
$Content =~ s{
\[% -? \s* RenderBlockStart \( \s* ['"]? (.*?) ['"]? \s* \) \s* -? %\]
}{[% PerformRenderBlock("$1") %][% BLOCK "$1" -%]}smxg;
$Content =~ s{
\[% -? \s* RenderBlockEnd \( \s* ['"]? (.*?) ['"]? \s* \) \s* -? %\]
}{[% END -%]}smxg;
#
# Add challenge token field to all internal forms
#
# (?!...) is a negative look-ahead, so "not followed by https?:"
# \K is a new feature in perl 5.10 which excludes anything prior
# to it from being included in the match, which means the string
# matched before it is not being replaced away.
# performs better than including $1 in the substitution.
#
$Content =~ s{
<form[^<>]+action="(?!https?:)[^"]*"[^<>]*>\K
}{[% IF Env("UserChallengeToken") %]<input type="hidden" name="ChallengeToken" value="[% Env("UserChallengeToken") | html %]"/>[% END %][% IF Env("SessionID") && !Env("SessionIDCookie") %]<input type="hidden" name="[% Env("SessionName") %]" value="[% Env("SessionID") | html %]"/>[% END %]}smxig;
return $Content;
}
=end Internal:
=head2 MigrateDTLtoTT()
translates old C<DTL> template content to L<Template::Toolkit> syntax.
my $TTCode = $ProviderObject->MigrateDTLtoTT( Content => $DTLCode );
If an error was found, this method will C<die()>, so please use eval around it.
=cut
sub MigrateDTLtoTT {
my ( $Self, %Param ) = @_;
my $Content = $Param{Content};
my $ID = "[a-zA-Z0-9:_\-]+";
my $SafeArrrayAccess = sub {
my $ID = shift;
if ( $ID !~ m{^[a-zA-Z0-9_]+$}xms ) {
return "item(\"$ID\")";
}
return $ID;
};
# $Quote $Config
$Content =~ s{\$Quote[{]"\$Config[{]"($ID)"}"}}{[% Config("$1") | html %]}smxg;
# $Quote $Env
$Content =~ s{\$Quote[{]"\$Env[{]"($ID)"}"}}{[% Env("$1") | html %]}smxg;
# $Quote $Data
$Content =~ s{
\$Quote[{]"\$Data[{]"($ID)"}"}
}
{
'[% Data.' . $SafeArrrayAccess->($1) . ' | html %]'
}esmxg;
# $Quote with length
$Content =~ s{
\$Quote[{]"\$Data[{]"($ID)"}",\s*"(\d+)"}
}
{
'[% Data.' . $SafeArrrayAccess->($1) . " | truncate($2) | html %]"
}esmxg;
# $Quote with dynamic length
$Content =~ s{
\$Quote[{]"\$Data[{]"($ID)"}",\s*"\$Q?Data[{]"($ID)"}"}
}
{
'[% Data.' . $SafeArrrayAccess->($1) . ' | truncate(Data.' . $SafeArrrayAccess->($2) . ') | html %]'
}esmxg;
# $Quote with translated text and fixed length
$Content =~ s{
\$Quote[{]"\$Text[{]"\$Data[{]"($ID)"}"}",\s*"(\d+)"}
}
{
'[% Data.' . $SafeArrrayAccess->($1) . " | Translate | truncate($2) | html %]"
}esmxg;
# $Quote with translated text and dynamic length
$Content =~ s{
\$Quote[{]"\$Text[{]"\$Data[{]"($ID)"}"}",\s*"\$Q?Data[{]"($ID)"}"}
}
{
'[% Data.' . $SafeArrrayAccess->($1) . ' | Translate | truncate(Data.' . $SafeArrrayAccess->($2) . ') | html %]'
}esmxg;
my $MigrateTextTag = sub {
my %Param = @_;
my $Mode = $Param{Mode}; # HTML or JSON
my $Text = $Param{Text}; # The translated text
my $Dot = $Param{Dot}; # Closing dot, sometimes outside of the Tag
my $ParamString = $Param{Parameters}; # Parameters to interpolate
my $Result = '[% ';
# Text contains a tag
if ( $Text =~ m{\$TimeLong[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
$Result .= "Translate(Localize(Data." . $SafeArrrayAccess->($1) . ", \"TimeLong\")";
}
elsif ( $Text =~ m{\$TimeShort[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
$Result .= "Translate(Localize(Data." . $SafeArrrayAccess->($1) . ", \"TimeShort\")";
}
elsif ( $Text =~ m{\$Date[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
$Result .= "Translate(Localize(Data." . $SafeArrrayAccess->($1) . ", \"Date\")";
}
elsif ( $Text =~ m{\$Q?Data[{]"($ID)"}}smx ) {
$Result .= "Translate(Data." . $SafeArrrayAccess->($1) . "";
}
elsif ( $Text =~ m{\$Config[{]"($ID)"}}smx ) {
$Result .= "Translate(Config(\"$1\")";
}
elsif ( $Text =~ m{\$Q?Env[{]"($ID)"}}smx ) {
$Result .= "Translate(Env(\"$1\")";
}
# Plain text
else {
$Text =~ s{"}{\\"}smxg; # Escape " signs
if ( $Param{Dot} ) {
$Text .= $Param{Dot};
}
$Result .= "Translate(\"$Text\"";
}
my @Parameters = split m{,\s*}, $ParamString;
PARAMETER:
for my $Parameter (@Parameters) {
next PARAMETER if ( !$Parameter );
if ( $Parameter =~ m{\$TimeLong[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
$Result .= ", Localize(Data.$1, \"TimeLong\")";
}
elsif ( $Parameter =~ m{\$TimeShort[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
$Result .= ", Localize(Data.$1, \"TimeShort\")";
}
elsif ( $Parameter =~ m{\$Date[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
$Result .= ", Localize(Data.$1, \"Date\")";
}
elsif ( $Parameter =~ m{\$Q?Data[{]"($ID)"}}smx ) {
$Result .= ", Data.$1";
}
elsif ( $Parameter =~ m{\$Config[{]"($ID)"}}smx ) {
$Result .= ", Config(\"$1\")";
}
elsif ( $Parameter =~ m{\$Q?Env[{]"($ID)"}}smx ) {
$Result .= ", Env(\"$1\")";
}
else {
$Parameter =~ s{^"|"$}{}smxg; # Remove enclosing ""
$Parameter =~ s{"}{\\"}smxg; # Escape " signs in the string
$Result .= ", \"$Parameter\"";
}
}
if ( $Mode eq 'JSON' ) {
$Result .= ') | JSON %]';
}
else {
$Result .= ') | html %]';
}
return $Result;
};
my $TextOrData = "";
# $Text
$Content =~ s{
\$Text[{]
["']
(
[^\$]+?
|\$Q?Data[{]\"$ID\"}
|\$Config[{]\"$ID\"}
|\$Q?Env[{]\"$ID\"}
|\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
)
["']
((?:
,\s*["']
(?:
[^\$]+?
|\$Q?Data[{]\"$ID\"}
|\$Config[{]\"$ID\"}
|\$Q?Env[{]\"$ID\"}
|\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
)
["'])*)
}
}
{
$MigrateTextTag->( Mode => 'HTML', Text => $1, Parameters => $2);
}esmxg;
# drop empty $Text
$Content =~ s{ \$Text [{] "" [}] }{}xmsg;
# $JSText
$Content =~ s{
["']\$JSText[{]
["']
(
[^\$]+?
|\$Q?Data[{]\"$ID\"}
|\$Config[{]\"$ID\"}
|\$Q?Env[{]\"$ID\"}
|\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
)
["']
((?:
,\s*["']
(?:
[^\$]+?
|\$Q?Data[{]\"$ID\"}
|\$Config[{]\"$ID\"}
|\$Q?Env[{]\"$ID\"}
|\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
|\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
)
["'])*)
}
(.?)["']
}
{
$MigrateTextTag->( Mode => 'JSON', Text => $1, Parameters => $2, Dot => $3);
}esmxg;
# $TimeLong
$Content =~ s{\$TimeLong[{]"\$Q?Data[{]"($ID)"}"}}{[% Data.$1 | Localize("TimeLong") %]}smxg;
# $TimeShort
$Content =~ s{\$TimeShort[{]"\$Q?Data[{]"($ID)"}"}}{[% Data.$1 | Localize("TimeShort") %]}smxg;
# $Date
$Content =~ s{\$Date[{]"\$Q?Data[{]"($ID)"}"}}{[% Data.$1 | Localize("Date") %]}smxg;
# $QData with length
$Content =~ s{
\$QData[{]"($ID)",\s*"(\d+)"}
}
{
"[% Data." . $SafeArrrayAccess->($1) . " | truncate($2) | html %]"
}esmxg;
# simple $QData
$Content =~ s{
\$QData[{]"($ID)"}
}
{
"[% Data." . $SafeArrrayAccess->($1) . " | html %]"
}esmxg;
# $LQData
$Content =~ s{
\$LQData[{]"($ID)"}
}
{
"[% Data." . $SafeArrrayAccess->($1) . " | uri %]"
}esmxg;
# simple $Data
$Content =~ s{
\$Data[{]"($ID)"}
}
{
"[% Data." . $SafeArrrayAccess->($1) . " %]"
}esmxg;
# $Config
$Content =~ s{\$Config[{]"($ID)"}}{[% Config("$1") %]}smxg;
# $Env
$Content =~ s{\$Env[{]"($ID)"}}{[% Env("$1") %]}smxg;
# $QEnv
$Content =~ s{\$QEnv[{]"($ID)"}}{[% Env("$1") | html %]}smxg;
# dtl:block
my %BlockSeen;
$Content =~ s{<!--\s*dtl:block:($ID)\s*-->}{
if ($BlockSeen{$1}++ % 2) {
"[% RenderBlockEnd(\"$1\") %]";
}
else {
"[% RenderBlockStart(\"$1\") %]";
}
}esmxg;
# dtl:js_on_document_complete
$Content =~ s{
<!--\s*dtl:js_on_document_complete\s*-->(.*?)<!--\s*dtl:js_on_document_complete\s*-->
}
{
"[% WRAPPER JSOnDocumentComplete %]${1}[% END %]";
}esmxg;
# dtl:js_on_document_complete_insert
$Content
=~ s{<!--\s*dtl:js_on_document_complete_placeholder\s*-->}{[% PROCESS JSOnDocumentCompleteInsert %]}smxg;
# $Include
$Content =~ s{\$Include[{]"($ID)"}}{[% InsertTemplate("$1.tt") %]}smxg;
my ( $Counter, $ErrorMessage );
LINE:
for my $Line ( split /\n/, $Content ) {
$Counter++;
# Make sure there are no more DTL tags present in the code.
if ( $Line =~ m{\$(?:L?Q?Data|Quote|Config|Q?Env|Time|Date|Text|JSText|Include)\{}xms ) {
$ErrorMessage .= "Line $Counter: $Line\n";
}
}
die $ErrorMessage if $ErrorMessage;
return $Content;
}
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