1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::Output::Template::Provider;
10## no critic(Perl::Critic::Policy::OTRS::RequireCamelCase)
11## nofilter(TidyAll::Plugin::OTRS::Perl::SyntaxCheck)
12
13use strict;
14use warnings;
15
16use parent qw (Template::Provider);
17
18use Scalar::Util qw();
19use Template::Constants;
20
21use Kernel::Output::Template::Document;
22
23our @ObjectDependencies = (
24    'Kernel::Config',
25    'Kernel::System::Cache',
26    'Kernel::System::Encode',
27    'Kernel::System::Log',
28    'Kernel::System::Main',
29);
30
31# Force the use of our own document class.
32$Template::Provider::DOCUMENT = 'Kernel::Output::Template::Document';
33
34=head1 NAME
35
36Kernel::Output::Template::Provider - Template Toolkit custom provider
37
38=head1 PUBLIC INTERFACE
39
40=head2 OTRSInit()
41
42performs some post-initialization and creates a bridge between Template::Toolkit
43and OTRS by adding the OTRS objects to the Provider object. This method must be
44called after instantiating the Provider object.
45
46Please note that we only store a weak reference to the LayoutObject to avoid ring
47references.
48
49=cut
50
51sub OTRSInit {
52    my ( $Self, %Param ) = @_;
53
54    # Don't fetch LayoutObject via ObjectManager as there might be several instances involved
55    #   at this point (for example in LinkObject there is an own LayoutObject to avoid block
56    #   name collisions).
57    $Self->{LayoutObject} = $Param{LayoutObject} || die "Got no LayoutObject!";
58
59    #
60    # Store a weak reference to the LayoutObject to avoid ring references.
61    #   We need it for the filters.
62    #
63    Scalar::Util::weaken( $Self->{LayoutObject} );
64
65    # define cache type
66    $Self->{CacheType} = 'TemplateProvider';
67
68    # caching can be disabled for debugging reasons
69    $Self->{CachingEnabled} = $Kernel::OM->Get('Kernel::Config')->Get('Frontend::TemplateCache') // 1;
70
71    return;
72}
73
74=begin Internal:
75
76=head2 _fetch()
77
78try to get a compiled version of a template from the CacheObject,
79otherwise compile the template and return it.
80
81Copied and slightly adapted from Template::Provider.
82
83A note about caching: we have three levels of caching.
84
85    1. we have an in-memory cache that stores the compiled Document objects (fastest).
86    2. we store the parsed data in the CacheObject to be re-used in another request.
87    3. for string templates, we have an in-memory cache in the parsing method _compile().
88        It will return the already parsed object if it sees the same template content again.
89
90=cut
91
92sub _fetch {
93    my ( $self, $name, $t_name ) = @_;
94    my $stat_ttl = $self->{STAT_TTL};
95
96    $self->debug("_fetch($name)") if $self->{DEBUG};
97
98    # Check in-memory template cache if we already had this template.
99    $self->{_TemplateCache} //= {};
100
101    if ( $self->{_TemplateCache}->{$name} ) {
102        return $self->{_TemplateCache}->{$name};
103    }
104
105    # See if we already know the template is not found
106    if ( $self->{NOTFOUND}->{$name} ) {
107        return ( undef, Template::Constants::STATUS_DECLINED );
108    }
109
110    # Check if the template exists, is cacheable and if a cached version exists.
111    if ( -e $name && $self->{CachingEnabled} ) {
112
113        my $UserTheme      = $self->{LayoutObject}->{EnvRef}->{UserTheme};
114        my $template_mtime = $self->_template_modified($name);
115        my $CacheKey       = $self->_compiled_filename($name) . '::' . $template_mtime . '::' . $UserTheme;
116
117        # Is there an up-to-date compiled version in the cache?
118        my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
119            Type => $self->{CacheType},
120            Key  => $CacheKey,
121        );
122
123        if ( ref $Cache ) {
124
125            my $compiled_template = $Template::Provider::DOCUMENT->new($Cache);
126
127            # Store in-memory and return the compiled template
128            if ($compiled_template) {
129
130                # Make sure template cache does not get too big
131                if ( keys %{ $self->{_TemplateCache} } > 1000 ) {
132                    $self->{_TemplateCache} = {};
133                }
134
135                $self->{_TemplateCache}->{$name} = $compiled_template;
136
137                return $compiled_template;
138            }
139
140            # Problem loading compiled template: warn and continue to fetch source template
141            warn( $self->error(), "\n" );
142        }
143    }
144
145    # load template from source
146    my ( $template, $error ) = $self->_load( $name, $t_name );
147
148    if ($error) {
149
150        # Template could not be fetched.  Add to the negative/notfound cache.
151        $self->{NOTFOUND}->{$name} = time;
152        return ( $template, $error );
153    }
154
155    # compile template source
156    ( $template, $error ) = $self->_compile( $template, $self->_compiled_filename($name) );
157
158    if ($error) {
159
160        # return any compile time error
161        return ( $template, $error );
162    }
163
164    # Make sure template cache does not get too big
165    if ( keys %{ $self->{_TemplateCache} } > 1000 ) {
166        $self->{_TemplateCache} = {};
167    }
168
169    $self->{_TemplateCache}->{$name} = $template->{data};
170
171    return $template->{data};
172
173}
174
175=head2 _load()
176
177calls our pre processor when loading a template.
178
179Inherited from Template::Provider.
180
181=cut
182
183sub _load {
184    my ( $Self, $Name, $Alias ) = @_;
185
186    my @Result = $Self->SUPER::_load( $Name, $Alias );
187
188    # If there was no error, pre-process our template
189    if ( ref $Result[0] ) {
190
191        $Result[0]->{text} = $Self->_PreProcessTemplateContent(
192            Content      => $Result[0]->{text},
193            TemplateFile => $Result[0]->{name},
194        );
195    }
196
197    return @Result;
198}
199
200=head2 _compile()
201
202compiles a .tt template into a Perl package and uses the CacheObject
203to cache it.
204
205Copied and slightly adapted from Template::Provider.
206
207=cut
208
209sub _compile {
210    my ( $self, $data, $compfile ) = @_;
211    my $text = $data->{text};
212    my ( $parsedoc, $error );
213
214    if ( $self->{DEBUG} ) {
215        $self->debug(
216            "_compile($data, ",
217            defined $compfile ? $compfile : '<no compfile>', ')'
218        );
219    }
220
221    # Check in-memory parser cache if we already had this template content
222    $self->{_ParserCache} //= {};
223
224    if ( $self->{_ParserCache}->{$text} ) {
225        return $self->{_ParserCache}->{$text};
226    }
227
228    my $parser = $self->{PARSER}
229        ||= Template::Config->parser( $self->{PARAMS} )
230        || return ( Template::Config->error(), Template::Constants::STATUS_ERROR );
231
232    # discard the template text - we don't need it any more
233    delete $data->{text};
234
235    # call parser to compile template into Perl code
236    if ( $parsedoc = $parser->parse( $text, $data ) ) {
237
238        $parsedoc->{METADATA} = {
239            'name'    => $data->{name},
240            'modtime' => $data->{time},
241            %{ $parsedoc->{METADATA} },
242        };
243
244        # write the Perl code to the file $compfile, if defined
245        if ($compfile) {
246            my $UserTheme = $self->{LayoutObject}->{EnvRef}->{UserTheme};
247            my $CacheKey  = $compfile . '::' . $data->{time} . '::' . $UserTheme;
248
249            if ( $self->{CachingEnabled} ) {
250                $Kernel::OM->Get('Kernel::System::Cache')->Set(
251                    Type  => $self->{CacheType},
252                    TTL   => 60 * 60 * 24,
253                    Key   => $CacheKey,
254                    Value => $parsedoc,
255                );
256            }
257        }
258
259        if ( $data->{data} = $Template::Provider::DOCUMENT->new($parsedoc) ) {
260
261            # Make sure parser cache does not get too big
262            if ( keys %{ $self->{_ParserCache} } > 1000 ) {
263                $self->{_ParserCache} = {};
264            }
265
266            $self->{_ParserCache}->{$text} = $data;
267
268            return $data;
269        }
270        $error = $Template::Document::ERROR;
271    }
272    else {
273        $error = Template::Exception->new( 'parse', "$data->{ name } " . $parser->error() );
274    }
275
276    # return STATUS_ERROR, or STATUS_DECLINED if we're being tolerant
277    return $self->{TOLERANT}
278        ? ( undef, Template::Constants::STATUS_DECLINED )
279        : ( $error, Template::Constants::STATUS_ERROR );
280}
281
282=end Internal:
283
284=head2 store()
285
286inherited from Template::Provider. This function override just makes sure that the original
287in-memory cache cannot be used.
288
289=cut
290
291sub store {
292    my ( $Self, $Name, $Data ) = @_;
293
294    return $Data;    # no-op
295}
296
297=begin Internal:
298
299=head2 _PreProcessTemplateContent()
300
301this is our template pre processor.
302
303It handles some OTRS specific tags like [% InsertTemplate("TemplateName.tt") %]
304and also performs compile-time code injection (ChallengeToken element into forms).
305
306Besides that, it also makes sure the template is treated as UTF8.
307
308This is run at compile time. If a template is cached, this method does not have to be executed on it
309any more.
310
311=cut
312
313sub _PreProcessTemplateContent {
314    my ( $Self, %Param ) = @_;
315
316    my $Content = $Param{Content};
317
318    # Make sure the template is treated as utf8.
319    $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( \$Content );
320
321    my $TemplateFileWithoutTT = substr( $Param{TemplateFile}, 0, -3 );
322
323    #
324    # Include other templates into this one before parsing.
325    # [% IncludeTemplate("DatePicker.tt") %]
326    #
327    my ( $ReplaceCounter, $Replaced );
328    do {
329        $Replaced = $Content =~ s{
330            \[% -? \s* InsertTemplate \( \s* ['"]? (.*?) ['"]? \s* \) \s* -? %\]\n?
331            }{
332                # Load the template via the provider.
333                # We'll use SUPER::load here because we don't need the preprocessing twice.
334                my $TemplateContent = ($Self->SUPER::load($1))[0];
335                $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput(\$TemplateContent);
336
337                # Remove commented lines already here because of problems when the InsertTemplate tag
338                #   is not on the beginning of the line.
339                $TemplateContent =~ s/^#.*\n//gm;
340                $TemplateContent;
341            }esmxg;
342
343    } until ( !$Replaced || ++$ReplaceCounter > 100 );
344
345    #
346    # Remove DTL-style comments (lines starting with #)
347    #
348    $Content =~ s/^#.*\n//gm if !$ENV{TEMPLATE_KEEP_COMMENTS};
349
350    #
351    # Insert a BLOCK call into the template.
352    # [% RenderBlock('b1') %]...[% END %]
353    # becomes
354    # [% PerformRenderBlock('b1') %][% BLOCK 'b1' %]...[% END %]
355    # This is what we need: define the block and call it from the RenderBlock macro
356    # to render it based on available block data from the frontend modules.
357    #
358    $Content =~ s{
359        \[% -? \s* RenderBlockStart \( \s* ['"]? (.*?) ['"]? \s* \) \s* -? %\]
360        }{[% PerformRenderBlock("$1") %][% BLOCK "$1" -%]}smxg;
361
362    $Content =~ s{
363        \[% -? \s* RenderBlockEnd \( \s* ['"]? (.*?) ['"]? \s* \) \s* -? %\]
364        }{[% END -%]}smxg;
365
366    #
367    # Add challenge token field to all internal forms
368    #
369    # (?!...) is a negative look-ahead, so "not followed by https?:"
370    # \K is a new feature in perl 5.10 which excludes anything prior
371    # to it from being included in the match, which means the string
372    # matched before it is not being replaced away.
373    # performs better than including $1 in the substitution.
374    #
375    $Content =~ s{
376            <form[^<>]+action="(?!https?:)[^"]*"[^<>]*>\K
377        }{[% 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;
378
379    return $Content;
380
381}
382
383=end Internal:
384
385=head2 MigrateDTLtoTT()
386
387translates old C<DTL> template content to L<Template::Toolkit> syntax.
388
389    my $TTCode = $ProviderObject->MigrateDTLtoTT( Content => $DTLCode );
390
391If an error was found, this method will C<die()>, so please use eval around it.
392
393=cut
394
395sub MigrateDTLtoTT {
396    my ( $Self, %Param ) = @_;
397
398    my $Content = $Param{Content};
399
400    my $ID = "[a-zA-Z0-9:_\-]+";
401
402    my $SafeArrrayAccess = sub {
403        my $ID = shift;
404        if ( $ID !~ m{^[a-zA-Z0-9_]+$}xms ) {
405            return "item(\"$ID\")";
406        }
407        return $ID;
408    };
409
410    # $Quote $Config
411    $Content =~ s{\$Quote[{]"\$Config[{]"($ID)"}"}}{[% Config("$1") | html %]}smxg;
412
413    # $Quote $Env
414    $Content =~ s{\$Quote[{]"\$Env[{]"($ID)"}"}}{[% Env("$1") | html %]}smxg;
415
416    # $Quote $Data
417    $Content =~ s{
418            \$Quote[{]"\$Data[{]"($ID)"}"}
419        }
420        {
421            '[% Data.' . $SafeArrrayAccess->($1) . ' | html %]'
422        }esmxg;
423
424    # $Quote with length
425    $Content =~ s{
426            \$Quote[{]"\$Data[{]"($ID)"}",\s*"(\d+)"}
427        }
428        {
429            '[% Data.' . $SafeArrrayAccess->($1) . " | truncate($2) | html %]"
430        }esmxg;
431
432    # $Quote with dynamic length
433    $Content =~ s{
434            \$Quote[{]"\$Data[{]"($ID)"}",\s*"\$Q?Data[{]"($ID)"}"}
435        }
436        {
437            '[% Data.' . $SafeArrrayAccess->($1) . ' | truncate(Data.' . $SafeArrrayAccess->($2) . ') | html %]'
438        }esmxg;
439
440    # $Quote with translated text and fixed length
441    $Content =~ s{
442            \$Quote[{]"\$Text[{]"\$Data[{]"($ID)"}"}",\s*"(\d+)"}
443        }
444        {
445            '[% Data.' . $SafeArrrayAccess->($1) . " | Translate | truncate($2) | html %]"
446        }esmxg;
447
448    # $Quote with translated text and dynamic length
449    $Content =~ s{
450            \$Quote[{]"\$Text[{]"\$Data[{]"($ID)"}"}",\s*"\$Q?Data[{]"($ID)"}"}
451        }
452        {
453            '[% Data.' . $SafeArrrayAccess->($1) . ' | Translate | truncate(Data.' . $SafeArrrayAccess->($2) . ') | html %]'
454        }esmxg;
455
456    my $MigrateTextTag = sub {
457        my %Param       = @_;
458        my $Mode        = $Param{Mode};          # HTML or JSON
459        my $Text        = $Param{Text};          # The translated text
460        my $Dot         = $Param{Dot};           # Closing dot, sometimes outside of the Tag
461        my $ParamString = $Param{Parameters};    # Parameters to interpolate
462
463        my $Result = '[% ';
464
465        # Text contains a tag
466        if ( $Text =~ m{\$TimeLong[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
467            $Result .= "Translate(Localize(Data." . $SafeArrrayAccess->($1) . ", \"TimeLong\")";
468        }
469        elsif ( $Text =~ m{\$TimeShort[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
470            $Result .= "Translate(Localize(Data." . $SafeArrrayAccess->($1) . ", \"TimeShort\")";
471        }
472        elsif ( $Text =~ m{\$Date[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
473            $Result .= "Translate(Localize(Data." . $SafeArrrayAccess->($1) . ", \"Date\")";
474        }
475        elsif ( $Text =~ m{\$Q?Data[{]"($ID)"}}smx ) {
476            $Result .= "Translate(Data." . $SafeArrrayAccess->($1) . "";
477        }
478        elsif ( $Text =~ m{\$Config[{]"($ID)"}}smx ) {
479            $Result .= "Translate(Config(\"$1\")";
480        }
481        elsif ( $Text =~ m{\$Q?Env[{]"($ID)"}}smx ) {
482            $Result .= "Translate(Env(\"$1\")";
483        }
484
485        # Plain text
486        else {
487            $Text =~ s{"}{\\"}smxg;    # Escape " signs
488            if ( $Param{Dot} ) {
489                $Text .= $Param{Dot};
490            }
491            $Result .= "Translate(\"$Text\"";
492        }
493
494        my @Parameters = split m{,\s*}, $ParamString;
495
496        PARAMETER:
497        for my $Parameter (@Parameters) {
498            next PARAMETER if ( !$Parameter );
499            if ( $Parameter =~ m{\$TimeLong[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
500                $Result .= ", Localize(Data.$1, \"TimeLong\")";
501            }
502            elsif ( $Parameter =~ m{\$TimeShort[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
503                $Result .= ", Localize(Data.$1, \"TimeShort\")";
504            }
505            elsif ( $Parameter =~ m{\$Date[{]"\$Q?Data[{]"($ID)"}"}}smx ) {
506                $Result .= ", Localize(Data.$1, \"Date\")";
507            }
508            elsif ( $Parameter =~ m{\$Q?Data[{]"($ID)"}}smx ) {
509                $Result .= ", Data.$1";
510            }
511            elsif ( $Parameter =~ m{\$Config[{]"($ID)"}}smx ) {
512                $Result .= ", Config(\"$1\")";
513            }
514            elsif ( $Parameter =~ m{\$Q?Env[{]"($ID)"}}smx ) {
515                $Result .= ", Env(\"$1\")";
516            }
517            else {
518                $Parameter =~ s{^"|"$}{}smxg;    # Remove enclosing ""
519                $Parameter =~ s{"}{\\"}smxg;     # Escape " signs in the string
520                $Result .= ", \"$Parameter\"";
521            }
522        }
523
524        if ( $Mode eq 'JSON' ) {
525            $Result .= ') | JSON %]';
526        }
527        else {
528            $Result .= ') | html %]';
529        }
530
531        return $Result;
532    };
533
534    my $TextOrData = "";
535
536    # $Text
537    $Content =~ s{
538            \$Text[{]
539                ["']
540                (
541                    [^\$]+?
542                    |\$Q?Data[{]\"$ID\"}
543                    |\$Config[{]\"$ID\"}
544                    |\$Q?Env[{]\"$ID\"}
545                    |\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
546                    |\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
547                    |\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
548                )
549                ["']
550                ((?:
551                    ,\s*["']
552                    (?:
553                        [^\$]+?
554                        |\$Q?Data[{]\"$ID\"}
555                        |\$Config[{]\"$ID\"}
556                        |\$Q?Env[{]\"$ID\"}
557                        |\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
558                        |\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
559                        |\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
560                    )
561                ["'])*)
562            }
563        }
564        {
565            $MigrateTextTag->( Mode => 'HTML', Text => $1, Parameters => $2);
566        }esmxg;
567
568    # drop empty $Text
569    $Content =~ s{ \$Text [{] "" [}] }{}xmsg;
570
571    # $JSText
572    $Content =~ s{
573            ["']\$JSText[{]
574                ["']
575                (
576                    [^\$]+?
577                    |\$Q?Data[{]\"$ID\"}
578                    |\$Config[{]\"$ID\"}
579                    |\$Q?Env[{]\"$ID\"}
580                    |\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
581                    |\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
582                    |\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
583                )
584                ["']
585                ((?:
586                    ,\s*["']
587                    (?:
588                        [^\$]+?
589                        |\$Q?Data[{]\"$ID\"}
590                        |\$Config[{]\"$ID\"}
591                        |\$Q?Env[{]\"$ID\"}
592                        |\$TimeLong[{]\"\$Q?Data[{]\"$ID\"}\"}
593                        |\$TimeShort[{]\"\$Q?Data[{]\"$ID\"}\"}
594                        |\$Date[{]\"\$Q?Data[{]\"$ID\"}\"}
595                    )
596                ["'])*)
597            }
598            (.?)["']
599        }
600        {
601            $MigrateTextTag->( Mode => 'JSON', Text => $1, Parameters => $2, Dot => $3);
602        }esmxg;
603
604    # $TimeLong
605    $Content =~ s{\$TimeLong[{]"\$Q?Data[{]"($ID)"}"}}{[% Data.$1 | Localize("TimeLong") %]}smxg;
606
607    # $TimeShort
608    $Content =~ s{\$TimeShort[{]"\$Q?Data[{]"($ID)"}"}}{[% Data.$1 | Localize("TimeShort") %]}smxg;
609
610    # $Date
611    $Content =~ s{\$Date[{]"\$Q?Data[{]"($ID)"}"}}{[% Data.$1 | Localize("Date") %]}smxg;
612
613    # $QData with length
614    $Content =~ s{
615            \$QData[{]"($ID)",\s*"(\d+)"}
616        }
617        {
618            "[% Data." . $SafeArrrayAccess->($1) . " | truncate($2) | html %]"
619        }esmxg;
620
621    # simple $QData
622    $Content =~ s{
623            \$QData[{]"($ID)"}
624        }
625        {
626            "[% Data." . $SafeArrrayAccess->($1) . " | html %]"
627        }esmxg;
628
629    # $LQData
630    $Content =~ s{
631            \$LQData[{]"($ID)"}
632        }
633        {
634            "[% Data." . $SafeArrrayAccess->($1) . " | uri %]"
635        }esmxg;
636
637    # simple $Data
638    $Content =~ s{
639            \$Data[{]"($ID)"}
640        }
641        {
642            "[% Data." . $SafeArrrayAccess->($1) . " %]"
643        }esmxg;
644
645    # $Config
646    $Content =~ s{\$Config[{]"($ID)"}}{[% Config("$1") %]}smxg;
647
648    # $Env
649    $Content =~ s{\$Env[{]"($ID)"}}{[% Env("$1") %]}smxg;
650
651    # $QEnv
652    $Content =~ s{\$QEnv[{]"($ID)"}}{[% Env("$1") | html %]}smxg;
653
654    # dtl:block
655    my %BlockSeen;
656    $Content =~ s{<!--\s*dtl:block:($ID)\s*-->}{
657        if ($BlockSeen{$1}++ % 2) {
658            "[% RenderBlockEnd(\"$1\") %]";
659        }
660        else {
661            "[% RenderBlockStart(\"$1\") %]";
662        }
663    }esmxg;
664
665    # dtl:js_on_document_complete
666    $Content =~ s{
667            <!--\s*dtl:js_on_document_complete\s*-->(.*?)<!--\s*dtl:js_on_document_complete\s*-->
668        }
669        {
670            "[% WRAPPER JSOnDocumentComplete %]${1}[% END %]";
671        }esmxg;
672
673    # dtl:js_on_document_complete_insert
674    $Content
675        =~ s{<!--\s*dtl:js_on_document_complete_placeholder\s*-->}{[% PROCESS JSOnDocumentCompleteInsert %]}smxg;
676
677    # $Include
678    $Content =~ s{\$Include[{]"($ID)"}}{[% InsertTemplate("$1.tt") %]}smxg;
679
680    my ( $Counter, $ErrorMessage );
681    LINE:
682    for my $Line ( split /\n/, $Content ) {
683        $Counter++;
684
685        # Make sure there are no more DTL tags present in the code.
686        if ( $Line =~ m{\$(?:L?Q?Data|Quote|Config|Q?Env|Time|Date|Text|JSText|Include)\{}xms ) {
687            $ErrorMessage .= "Line $Counter: $Line\n";
688        }
689    }
690
691    die $ErrorMessage if $ErrorMessage;
692
693    return $Content;
694}
695
6961;
697
698=head1 TERMS AND CONDITIONS
699
700This software is part of the OTRS project (L<https://otrs.org/>).
701
702This software comes with ABSOLUTELY NO WARRANTY. For details, see
703the enclosed file COPYING for license information (GPL). If you
704did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
705
706=cut
707