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