2 namespace TYPO3\CMS\Core\TypoScript\Parser
;
5 * This file is part of the TYPO3 CMS project.
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use Psr\Log\LoggerInterface
;
18 use Symfony\Component\Finder\Finder
;
19 use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher
as BackendConditionMatcher
;
20 use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher
;
21 use TYPO3\CMS\Core\Core\Environment
;
22 use TYPO3\CMS\Core\Log\LogManager
;
23 use TYPO3\CMS\Core\TimeTracker\TimeTracker
;
24 use TYPO3\CMS\Core\TypoScript\ExtendedTemplateService
;
25 use TYPO3\CMS\Core\Utility\GeneralUtility
;
26 use TYPO3\CMS\Core\Utility\MathUtility
;
27 use TYPO3\CMS\Core\Utility\PathUtility
;
28 use TYPO3\CMS\Core\Utility\StringUtility
;
29 use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher
as FrontendConditionMatcher
;
32 * The TypoScript parser
34 class TypoScriptParser
37 * TypoScript hierarchy being build during parsing.
44 * Raw data, the input string exploded by LF
51 * Pointer to entry in raw data array
58 * Holding the value of the last comment
62 protected $lastComment = '';
65 * Internally set, used as internal flag to create a multi-line comment (one of those like /* ... * /
69 protected $commentSet = false
;
72 * Internally set, when multiline value is accumulated
76 protected $multiLineEnabled = false
;
79 * Internally set, when multiline value is accumulated
83 protected $multiLineObject = '';
86 * Internally set, when multiline value is accumulated
90 protected $multiLineValue = [];
93 * Internally set, when in brace. Counter.
97 protected $inBrace = 0;
100 * For each condition this flag is set, if the condition is TRUE,
101 * else it's cleared. Then it's used by the [ELSE] condition to determine if the next part should be parsed.
105 protected $lastConditionTrue = true
;
108 * Tracking all conditions found
112 public $sections = [];
115 * Tracking all matching conditions found
119 public $sectionsMatch = [];
122 * If set, then syntax highlight mode is on; Call the function syntaxHighlight() to use this function
126 protected $syntaxHighLight = false
;
129 * Syntax highlight data is accumulated in this array. Used by syntaxHighlight_print() to construct the output.
133 protected $highLightData = [];
136 * Syntax highlight data keeping track of the curly brace level for each line
140 protected $highLightData_bracelevel = [];
143 * DO NOT register the comments. This is default for the ordinary sitetemplate!
147 public $regComments = false
;
150 * DO NOT register the linenumbers. This is default for the ordinary sitetemplate!
154 public $regLinenumbers = false
;
157 * Error accumulation array.
164 * Used for the error messages line number reporting. Set externally.
168 public $lineNumberOffset = 0;
171 * Line for break point.
175 public $breakPointLN = 0;
180 protected $highLightStyles = [
181 'prespace' => ['<span class="ts-prespace">', '</span>'],
182 // Space before any content on a line
183 'objstr_postspace' => ['<span class="ts-objstr_postspace">', '</span>'],
184 // Space after the object string on a line
185 'operator_postspace' => ['<span class="ts-operator_postspace">', '</span>'],
186 // Space after the operator on a line
187 'operator' => ['<span class="ts-operator">', '</span>'],
189 'value' => ['<span class="ts-value">', '</span>'],
190 // The value of a line
191 'objstr' => ['<span class="ts-objstr">', '</span>'],
192 // The object string of a line
193 'value_copy' => ['<span class="ts-value_copy">', '</span>'],
194 // The value when the copy syntax (<) is used; that means the object reference
195 'value_unset' => ['<span class="ts-value_unset">', '</span>'],
196 // The value when an object is unset. Should not exist.
197 'ignored' => ['<span class="ts-ignored">', '</span>'],
198 // The "rest" of a line which will be ignored.
199 'default' => ['<span class="ts-default">', '</span>'],
200 // The default style if none other is applied.
201 'comment' => ['<span class="ts-comment">', '</span>'],
203 'condition' => ['<span class="ts-condition">', '</span>'],
205 'error' => ['<span class="ts-error">', '</span>'],
207 'linenum' => ['<span class="ts-linenum">', '</span>']
211 * Additional attributes for the <span> tags for a blockmode line
215 protected $highLightBlockStyles = '';
218 * The hex-HTML color for the blockmode
222 protected $highLightBlockStyles_basecolor = '#cccccc';
225 * @var \TYPO3\CMS\Core\TypoScript\ExtendedTemplateService
227 public $parentObject;
230 * Start parsing the input TypoScript text piece. The result is stored in $this->setup
232 * @param string $string The TypoScript text
233 * @param object|string $matchObj If is object, then this is used to match conditions found in the TypoScript code. If matchObj not specified, then no conditions will work! (Except [GLOBAL])
235 public function parse($string, $matchObj = '')
237 $this->raw
= explode(LF
, $string);
241 if ($this->breakPointLN
&& $pre === '[_BREAK]') {
242 $this->error('Breakpoint at ' . ($this->lineNumberOffset +
$this->rawP
- 2) . ': Line content was "' . $this->raw
[$this->rawP
- 2] . '"', 1);
246 $this->error('Empty condition is always false, this does not make sense. At line ' . ($this->lineNumberOffset +
$this->rawP
- 1), 2);
249 $preUppercase = strtoupper($pre);
250 if ($pre[0] === '[' &&
251 ($preUppercase === '[GLOBAL]' ||
252 $preUppercase === '[END]' ||
253 !$this->lastConditionTrue
&& $preUppercase === '[ELSE]')
255 $pre = trim($this->parseSub($this->setup
));
256 $this->lastConditionTrue
= true
;
258 // We're in a specific section. Therefore we log this section
259 $specificSection = $preUppercase !== '[ELSE]';
260 if ($specificSection) {
261 $this->sections
[md5($pre)] = $pre;
263 if (is_object($matchObj) && $matchObj->match($pre) ||
$this->syntaxHighLight
) {
264 if ($specificSection) {
265 $this->sectionsMatch
[md5($pre)] = $pre;
267 $pre = trim($this->parseSub($this->setup
));
268 $this->lastConditionTrue
= true
;
270 $pre = $this->nextDivider();
271 $this->lastConditionTrue
= false
;
275 if ($this->inBrace
) {
276 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': The script is short of ' . $this->inBrace
. ' end brace(s)', 1);
278 if ($this->multiLineEnabled
) {
279 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': A multiline value section is not ended with a parenthesis!', 1);
281 $this->lineNumberOffset +
= count($this->raw
) +
1;
285 * Will search for the next condition. When found it will return the line content (the condition value) and have advanced the internal $this->rawP pointer to point to the next line after the condition.
287 * @return string The condition value
290 protected function nextDivider()
292 while (isset($this->raw
[$this->rawP
])) {
293 $line = trim($this->raw
[$this->rawP
]);
295 if ($line && $line[0] === '[') {
303 * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
305 * @param array $setup Reference to the setup array in which to accumulate the values.
306 * @return string|null Returns the string of the condition found, the exit signal or possible nothing (if it completed parsing with no interruptions)
308 protected function parseSub(array &$setup)
310 while (isset($this->raw
[$this->rawP
])) {
311 $line = ltrim($this->raw
[$this->rawP
]);
312 $lineP = $this->rawP
;
314 if ($this->syntaxHighLight
) {
315 $this->regHighLight('prespace', $lineP, strlen($line));
318 // By adding 1 we get that line processed
319 if ($this->breakPointLN
&& $this->lineNumberOffset +
$this->rawP
- 1 === $this->breakPointLN +
1) {
323 if (!$this->multiLineEnabled
&& strpos($line, '/*') === 0) {
324 $this->commentSet
= true
;
326 // If $this->multiLineEnabled we will go and get the line values here because we know, the first if() will be TRUE.
327 if (!$this->commentSet
&& ($line ||
$this->multiLineEnabled
)) {
328 // If multiline is enabled. Escape by ')'
329 if ($this->multiLineEnabled
) {
331 if (!empty($line[0]) && $line[0] === ')') {
332 if ($this->syntaxHighLight
) {
333 $this->regHighLight('operator', $lineP, strlen($line) - 1);
336 $this->multiLineEnabled
= false
;
337 $theValue = implode($this->multiLineValue
, LF
);
338 if (strpos($this->multiLineObject
, '.') !== false
) {
339 // Set the value deeper.
340 $this->setVal($this->multiLineObject
, $setup, [$theValue]);
342 // Set value regularly
343 $setup[$this->multiLineObject
] = $theValue;
344 if ($this->lastComment
&& $this->regComments
) {
345 $setup[$this->multiLineObject
. '..'] .= $this->lastComment
;
347 if ($this->regLinenumbers
) {
348 $setup[$this->multiLineObject
. '.ln..'][] = $this->lineNumberOffset +
$this->rawP
- 1;
352 if ($this->syntaxHighLight
) {
353 $this->regHighLight('value', $lineP);
355 $this->multiLineValue
[] = $this->raw
[$this->rawP
- 1];
357 } elseif ($this->inBrace
=== 0 && $line[0] === '[') {
358 // Beginning of condition (only on level zero compared to brace-levels
359 if ($this->syntaxHighLight
) {
360 $this->regHighLight('condition', $lineP);
364 // Return if GLOBAL condition is set - no matter what.
365 if ($line[0] === '[' && stripos($line, '[GLOBAL]') !== false
) {
366 if ($this->syntaxHighLight
) {
367 $this->regHighLight('condition', $lineP);
369 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': On return to [GLOBAL] scope, the script was short of ' . $this->inBrace
. ' end brace(s)', 1);
373 if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
374 // If not brace-end or comment
375 // Find object name string until we meet an operator
376 $varL = strcspn($line, "\t" . ' {=<>(');
377 // check for special ":=" operator
378 if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
381 // also remove tabs after the object string name
382 $objStrName = substr($line, 0, $varL);
383 if ($this->syntaxHighLight
) {
384 $this->regHighLight('objstr', $lineP, strlen(substr($line, $varL)));
386 if ($objStrName !== '') {
388 if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
389 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."');
391 $line = ltrim(substr($line, $varL));
392 if ($this->syntaxHighLight
) {
393 $this->regHighLight('objstr_postspace', $lineP, strlen($line));
395 $this->regHighLight('operator', $lineP, strlen($line) - 1);
396 $this->regHighLight('operator_postspace', $lineP, strlen(ltrim(substr($line, 1))));
400 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
402 // Checking for special TSparser properties (to change TS values at parsetime)
404 if ($line[0] === ':' && preg_match('/^:=\\s*([[:alpha:]]+)\\s*\\((.*)\\).*/', $line, $match)) {
406 $tsFuncArg = $match[2];
407 $val = $this->getVal($objStrName, $setup);
408 $currentValue = $val[0] ?? null
;
409 $tsFuncArg = str_replace(['\\\\', '\\n', '\\t'], ['\\', LF
, "\t"], $tsFuncArg);
410 $newValue = $this->executeValueModifier($tsFunc, $tsFuncArg, $currentValue);
411 if (isset($newValue)) {
412 $line = '= ' . $newValue;
417 if ($this->syntaxHighLight
) {
418 $this->regHighLight('value', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
420 if (strpos($objStrName, '.') !== false
) {
422 $value[0] = trim(substr($line, 1));
423 $this->setVal($objStrName, $setup, $value);
425 $setup[$objStrName] = trim(substr($line, 1));
426 if ($this->lastComment
&& $this->regComments
) {
428 $matchingCommentKey = $objStrName . '..';
429 if (isset($setup[$matchingCommentKey])) {
430 $setup[$matchingCommentKey] .= $this->lastComment
;
432 $setup[$matchingCommentKey] = $this->lastComment
;
435 if ($this->regLinenumbers
) {
436 $setup[$objStrName . '.ln..'][] = $this->lineNumberOffset +
$this->rawP
- 1;
442 if (strpos($objStrName, '.') !== false
) {
443 $exitSig = $this->rollParseSub($objStrName, $setup);
448 if (!isset($setup[$objStrName . '.'])) {
449 $setup[$objStrName . '.'] = [];
451 $exitSig = $this->parseSub($setup[$objStrName . '.']);
458 $this->multiLineObject
= $objStrName;
459 $this->multiLineEnabled
= true
;
460 $this->multiLineValue
= [];
463 if ($this->syntaxHighLight
) {
464 $this->regHighLight('value_copy', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
466 $theVal = trim(substr($line, 1));
467 if ($theVal[0] === '.') {
468 $res = $this->getVal(substr($theVal, 1), $setup);
470 $res = $this->getVal($theVal, $this->setup
);
472 // unserialize(serialize(...)) may look stupid but is needed because of some reference issues.
473 // See forge issue #76919 and functional test hasFlakyReferences()
474 $this->setVal($objStrName, $setup, unserialize(serialize($res)), 1);
477 if ($this->syntaxHighLight
) {
478 $this->regHighLight('value_unset', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
480 $this->setVal($objStrName, $setup, 'UNSET');
483 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
487 $this->lastComment
= '';
489 } elseif ($line[0] === '}') {
491 $this->lastComment
= '';
492 if ($this->syntaxHighLight
) {
493 $this->regHighLight('operator', $lineP, strlen($line) - 1);
495 if ($this->inBrace
< 0) {
496 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': An end brace is in excess.', 1);
502 if ($this->syntaxHighLight
) {
503 $this->regHighLight('comment', $lineP);
505 // Comment. The comments are concatenated in this temporary string:
506 if ($this->regComments
) {
507 $this->lastComment
.= rtrim($line) . LF
;
510 if (strpos($line, '### ERROR') === 0) {
511 $this->error(substr($line, 11));
516 if ($this->commentSet
) {
517 if ($this->syntaxHighLight
) {
518 $this->regHighLight('comment', $lineP);
520 if (strpos($line, '*/') !== false
) {
521 $this->commentSet
= false
;
529 * Executes operator functions, called from TypoScript
530 * example: page.10.value := appendString(!)
532 * @param string $modifierName TypoScript function called
533 * @param string $modifierArgument Function arguments; In case of multiple arguments, the method must split on its own
534 * @param string $currentValue Current TypoScript value
535 * @return string|null Modified result or null for no modification
537 protected function executeValueModifier($modifierName, $modifierArgument = null
, $currentValue = null
)
540 switch ($modifierName) {
541 case 'prependString':
542 $newValue = $modifierArgument . $currentValue;
545 $newValue = $currentValue . $modifierArgument;
548 $newValue = str_replace($modifierArgument, '', $currentValue);
550 case 'replaceString':
551 $modifierArgumentArray = explode('|', $modifierArgument, 2);
552 $fromStr = $modifierArgumentArray[0] ??
'';
553 $toStr = $modifierArgumentArray[1] ??
'';
554 $newValue = str_replace($fromStr, $toStr, $currentValue);
557 $newValue = ((string)$currentValue !== '' ?
$currentValue . ',' : '') . $modifierArgument;
559 case 'removeFromList':
560 $existingElements = GeneralUtility
::trimExplode(',', $currentValue);
561 $removeElements = GeneralUtility
::trimExplode(',', $modifierArgument);
562 if (!empty($removeElements)) {
563 $newValue = implode(',', array_diff($existingElements, $removeElements));
567 $elements = GeneralUtility
::trimExplode(',', $currentValue);
568 $newValue = implode(',', array_unique($elements));
571 $elements = GeneralUtility
::trimExplode(',', $currentValue);
572 $newValue = implode(',', array_reverse($elements));
575 $elements = GeneralUtility
::trimExplode(',', $currentValue);
576 $arguments = GeneralUtility
::trimExplode(',', $modifierArgument);
577 $arguments = array_map('strtolower', $arguments);
578 $sort_flags = SORT_REGULAR
;
579 if (in_array('numeric', $arguments)) {
580 $sort_flags = SORT_NUMERIC
;
581 // If the sorting modifier "numeric" is given, all values
582 // are checked and an exception is thrown if a non-numeric value is given
583 // otherwise there is a different behaviour between PHP7 and PHP 5.x
584 // See also the warning on http://us.php.net/manual/en/function.sort.php
585 foreach ($elements as $element) {
586 if (!is_numeric($element)) {
587 throw new \
InvalidArgumentException('The list "' . $currentValue . '" should be sorted numerically but contains a non-numeric value', 1438191758);
591 sort($elements, $sort_flags);
592 if (in_array('descending', $arguments)) {
593 $elements = array_reverse($elements);
595 $newValue = implode(',', $elements);
598 $environmentValue = getenv(trim($modifierArgument));
599 if ($environmentValue !== false
) {
600 $newValue = $environmentValue;
604 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName])) {
605 $hookMethod = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName];
606 $params = ['currentValue' => $currentValue, 'functionArgument' => $modifierArgument];
608 $newValue = GeneralUtility
::callUserFunction($hookMethod, $params, $fakeThis);
610 self
::getLogger()->warning('Missing function definition for ' . $modifierName . ' on TypoScript');
617 * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
618 * thus having to recursively call itself to get the value
620 * @param string $string The object sub-path, eg "thisprop.another_prot
621 * @param array $setup The local setup array from the function calling this function
622 * @return string Returns the exitSignal
625 protected function rollParseSub($string, array &$setup)
627 if ((string)$string === '') {
631 list($key, $remainingKey) = $this->parseNextKeySegment($string);
633 if (!isset($setup[$key])) {
636 $exitSig = $remainingKey === ''
637 ?
$this->parseSub($setup[$key])
638 : $this->rollParseSub($remainingKey, $setup[$key]);
639 return $exitSig ?
: '';
643 * Get a value/property pair for an object path in TypoScript, eg. "myobject.myvalue.mysubproperty".
644 * Here: Used by the "copy" operator, <
646 * @param string $string Object path for which to get the value
647 * @param array $setup Global setup code if $string points to a global object path. But if string is prefixed with "." then its the local setup array.
648 * @return array An array with keys 0/1 being value/property respectively
650 public function getVal($string, $setup)
652 if ((string)$string === '') {
656 list($key, $remainingKey) = $this->parseNextKeySegment($string);
657 $subKey = $key . '.';
658 if ($remainingKey === '') {
660 if (isset($setup[$key])) {
661 $retArr[0] = $setup[$key];
663 if (isset($setup[$subKey])) {
664 $retArr[1] = $setup[$subKey];
668 if ($setup[$subKey]) {
669 return $this->getVal($remainingKey, $setup[$subKey]);
676 * Setting a value/property of an object string in the setup array.
678 * @param string $string The object sub-path, eg "thisprop.another_prot
679 * @param array $setup The local setup array from the function calling this function.
680 * @param array|string $value The value/property pair array to set. If only one of them is set, then the other is not touched (unless $wipeOut is set, which it is when copies are made which must include both value and property)
681 * @param bool $wipeOut If set, then both value and property is wiped out when a copy is made of another value.
683 protected function setVal($string, array &$setup, $value, $wipeOut = false
)
685 if ((string)$string === '') {
689 list($key, $remainingKey) = $this->parseNextKeySegment($string);
690 $subKey = $key . '.';
691 if ($remainingKey === '') {
692 if ($value === 'UNSET') {
694 unset($setup[$subKey]);
695 if ($this->regLinenumbers
) {
696 $setup[$key . '.ln..'][] = ($this->lineNumberOffset +
$this->rawP
- 1) . '>';
702 unset($setup[$subKey]);
703 if ($this->regLinenumbers
) {
704 $setup[$key . '.ln..'][] = ($this->lineNumberOffset +
$this->rawP
- 1) . '<';
708 if (isset($value[0])) {
709 $setup[$key] = $value[0];
711 if (isset($value[1])) {
712 $setup[$subKey] = $value[1];
714 if ($this->lastComment
&& $this->regComments
) {
715 $setup[$key . '..'] .= $this->lastComment
;
717 if ($this->regLinenumbers
&& !$lnRegisDone) {
718 $setup[$key . '.ln..'][] = $this->lineNumberOffset +
$this->rawP
- 1;
722 if (!isset($setup[$subKey])) {
723 $setup[$subKey] = [];
725 $this->setVal($remainingKey, $setup[$subKey], $value);
730 * Determines the first key segment of a TypoScript key by searching for the first
731 * unescaped dot in the given key string.
733 * Since the escape characters are only needed to correctly determine the key
734 * segment any escape characters before the first unescaped dot are
735 * stripped from the key.
737 * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
738 * @return array Array with key segment and remaining part of $key
740 protected function parseNextKeySegment($key)
742 // if no dot is in the key, nothing to do
743 $dotPosition = strpos($key, '.');
744 if ($dotPosition === false
) {
748 if (strpos($key, '\\') !== false
) {
749 // backslashes are in the key, so we do further parsing
751 while ($dotPosition !== false
) {
752 if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' ||
$dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
755 // escaped dot found, continue
756 $dotPosition = strpos($key, '.', $dotPosition +
1);
759 if ($dotPosition === false
) {
760 // no regular dot found
764 if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
765 $keySegment = substr($key, 0, $dotPosition - 1);
767 $keySegment = substr($key, 0, $dotPosition);
769 $remainingKey = substr($key, $dotPosition +
1);
772 // fix key segment by removing escape sequences
773 $keySegment = str_replace('\\.', '.', $keySegment);
775 // no backslash in the key, we're fine off
776 list($keySegment, $remainingKey) = explode('.', $key, 2);
778 return [$keySegment, $remainingKey];
782 * Stacks errors/messages from the TypoScript parser into an internal array, $this->error
783 * If "TT" is a global object (as it is in the frontend when backend users are logged in) the message will be registered here as well.
785 * @param string $err The error message string
786 * @param int $num The error severity (in the scale of TimeTracker::setTSlogMessage: Approx: 2=warning, 1=info, 0=nothing, 3=fatal.)
788 protected function error($err, $num = 2)
790 $tt = $this->getTimeTracker();
792 $tt->setTSlogMessage($err, $num);
794 $this->errors
[] = [$err, $num, $this->rawP
- 1, $this->lineNumberOffset
];
798 * Checks the input string (un-parsed TypoScript) for include-commands ("<INCLUDE_TYPOSCRIPT: ....")
799 * Use: \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines()
801 * @param string $string Unparsed TypoScript
802 * @param int $cycle_counter Counter for detecting endless loops
803 * @param bool $returnFiles When set an array containing the resulting typoscript and all included files will get returned
804 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
805 * @return string|array Complete TypoScript with includes added.
808 public static function checkIncludeLines($string, $cycle_counter = 1, $returnFiles = false
, $parentFilenameOrPath = '')
811 if ($cycle_counter > 100) {
812 self
::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
816 'files' => $includedFiles
821 ### ERROR: Recursion!
826 if ($string !== null
) {
827 $string = StringUtility
::removeByteOrderMark($string);
830 // Checking for @import syntax imported files
831 $string = self
::addImportsFromExternalFiles($string, $cycle_counter, $returnFiles, $includedFiles, $parentFilenameOrPath);
833 // If no tags found, no need to do slower preg_split
834 if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== false
) {
835 $splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
836 $parts = preg_split($splitRegEx, LF
. $string . LF
, -1, PREG_SPLIT_DELIM_CAPTURE
);
837 // First text part goes through
838 $newString = $parts[0] . LF
;
839 $partCount = count($parts);
840 for ($i = 1; $i +
3 < $partCount; $i +
= 4) {
841 // $parts[$i] contains 'FILE' or 'DIR'
842 // $parts[$i+1] contains relative file or directory path to be included
843 // $parts[$i+2] optional properties of the INCLUDE statement
844 // $parts[$i+3] next part of the typoscript string (part in between include-tags)
845 $includeType = $parts[$i];
846 $filename = $parts[$i +
1];
847 $originalFilename = $filename;
848 $optionalProperties = $parts[$i +
2];
849 $tsContentsTillNextInclude = $parts[$i +
3];
852 $matches = preg_split('#(?i)condition\\s*=\\s*"((?:\\\\\\\\|\\\\"|[^\\"])*)"(\\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE
);
853 // If there was a condition
854 if (count($matches) > 1) {
855 // Unescape the condition
856 $condition = trim(stripslashes($matches[1]));
857 // If necessary put condition in square brackets
858 if ($condition[0] !== '[') {
859 $condition = '[' . $condition . ']';
862 /** @var AbstractConditionMatcher $conditionMatcher */
863 $conditionMatcher = null
;
864 if (TYPO3_REQUESTTYPE
& TYPO3_REQUESTTYPE_FE
) {
865 $conditionMatcher = GeneralUtility
::makeInstance(FrontendConditionMatcher
::class);
867 $conditionMatcher = GeneralUtility
::makeInstance(BackendConditionMatcher
::class);
870 // If it didn't match then proceed to the next include, but prepend next normal (not file) part to output string
871 if (!$conditionMatcher->match($condition)) {
872 $newString .= $tsContentsTillNextInclude . LF
;
877 // Resolve a possible relative paths if a parent file is given
878 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
879 $filename = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
882 // There must be a line-break char after - not sure why this check is necessary, kept it for being 100% backwards compatible
883 // An empty string is also ok (means that the next line is also a valid include_typoscript tag)
884 if (!preg_match('/(^\\s*\\r?\\n|^$)/', $tsContentsTillNextInclude)) {
885 $newString .= self
::typoscriptIncludeError('Invalid characters after <INCLUDE_TYPOSCRIPT: source="' . $includeType . ':' . $filename . '">-tag (rest of line must be empty).');
886 } elseif (strpos('..', $filename) !== false
) {
887 $newString .= self
::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
889 switch (strtolower($includeType)) {
891 self
::includeFile($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
894 self
::includeDirectory($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
897 $newString .= self
::typoscriptIncludeError('No valid option for INCLUDE_TYPOSCRIPT source property (valid options are FILE or DIR)');
900 // Prepend next normal (not file) part to output string
901 $newString .= $tsContentsTillNextInclude . LF
;
903 // load default TypoScript for content rendering templates like
904 // fluid_styled_content if those have been included through f.e.
905 // <INCLUDE_TYPOSCRIPT: source="FILE:EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript">
906 if (strpos(strtolower($filename), 'ext:') === 0) {
907 $filePointerPathParts = explode('/', substr($filename, 4));
909 // remove file part, determine whether to load setup or constants
910 list($includeType, ) = explode('.', array_pop($filePointerPathParts));
912 if (in_array($includeType, ['setup', 'constants'])) {
913 // adapt extension key to required format (no underscores)
914 $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
916 // load default TypoScript
917 $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
918 if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true
)) {
919 $newString .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
924 // Add a line break before and after the included code in order to make sure that the parser always has a LF.
925 $string = LF
. trim($newString) . LF
;
927 // When all included files should get returned, simply return a compound array containing
928 // the TypoScript with all "includes" processed and the files which got included
931 'typoscript' => $string,
932 'files' => $includedFiles
939 * Splits the unparsed TypoScript content into import statements
941 * @param string $typoScript unparsed TypoScript
942 * @param int $cycleCounter counter to stop recursion
943 * @param bool $returnFiles whether to populate the included Files or not
944 * @param array $includedFiles - by reference - if any included files are added, they are added here
945 * @param string $parentFilenameOrPath the current imported file to resolve relative paths - handled by reference
946 * @return string the unparsed TypoScript with included external files
948 protected static function addImportsFromExternalFiles($typoScript, $cycleCounter, $returnFiles, &$includedFiles, &$parentFilenameOrPath)
950 // Check for new syntax "@import 'EXT:bennilove/Configuration/TypoScript/*'"
951 if (strpos($typoScript, '@import \'') !== false ||
strpos($typoScript, '@import "') !== false
) {
952 $splitRegEx = '/\r?\n\s*@import\s[\'"]([^\'"]*)[\'"][\ \t]?/';
953 $parts = preg_split($splitRegEx, LF
. $typoScript . LF
, -1, PREG_SPLIT_DELIM_CAPTURE
);
954 // First text part goes through
955 $newString = $parts[0] . LF
;
956 $partCount = count($parts);
957 for ($i = 1; $i +
2 <= $partCount; $i +
= 2) {
958 $filename = $parts[$i];
959 $tsContentsTillNextInclude = $parts[$i +
1];
960 // Resolve a possible relative paths if a parent file is given
961 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
962 $filename = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
964 $newString .= self
::importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, $includedFiles);
965 // Prepend next normal (not file) part to output string
966 $newString .= $tsContentsTillNextInclude;
968 // Add a line break before and after the included code in order to make sure that the parser always has a LF.
969 $typoScript = LF
. trim($newString) . LF
;
975 * Include file $filename. Contents of the file will be returned, filename is added to &$includedFiles.
976 * Further include/import statements in the contents are processed recursively.
978 * @param string $filename Full absolute path+filename to the typoscript file to be included
979 * @param int $cycleCounter Counter for detecting endless loops
980 * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
981 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
982 * @return string the unparsed TypoScript content from external files
984 protected static function importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, array &$includedFiles)
986 if (strpos('..', $filename) !== false
) {
987 return self
::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
991 $absoluteFileName = GeneralUtility
::getFileAbsFileName($filename);
992 if ((string)$absoluteFileName === '') {
993 return self
::typoscriptIncludeError('Illegal filepath "' . $filename . '".');
996 $finder = new Finder();
998 // no recursive mode on purpose
1000 // no directories should be fetched
1004 // Search all files in the folder
1005 if (is_dir($absoluteFileName)) {
1006 $finder->in($absoluteFileName);
1007 // Used for the TypoScript comments
1008 $readableFilePrefix = $filename;
1011 // Apparently this is not a folder, so the restriction
1012 // is the folder so we restrict into this folder
1013 $finder->in(PathUtility
::dirname($absoluteFileName));
1014 if (!is_file($absoluteFileName)
1015 && strpos(PathUtility
::basename($absoluteFileName), '*') === false
1016 && substr(PathUtility
::basename($absoluteFileName), -11) !== '.typoscript') {
1017 $absoluteFileName .= '*.typoscript';
1019 $finder->name(PathUtility
::basename($absoluteFileName));
1020 $readableFilePrefix = PathUtility
::dirname($filename);
1021 } catch (\InvalidArgumentException
$e) {
1022 return self
::typoscriptIncludeError($e->getMessage());
1026 foreach ($finder as $fileObject) {
1027 // Clean filename output for comments
1028 $readableFileName = rtrim($readableFilePrefix, '/') . '/' . $fileObject->getFilename();
1029 $content .= LF
. '### @import \'' . $readableFileName . '\' begin ###' . LF
;
1030 // Check for allowed files
1031 if (!GeneralUtility
::verifyFilenameAgainstDenyPattern($fileObject->getFilename())) {
1032 $content .= self
::typoscriptIncludeError('File "' . $readableFileName . '" was not included since it is not allowed due to fileDenyPattern.');
1034 $includedFiles[] = $fileObject->getPathname();
1035 // check for includes in included text
1036 $included_text = self
::checkIncludeLines($fileObject->getContents(), $cycleCounter++
, $returnFiles, $absoluteFileName);
1037 // If the method also has to return all included files, merge currently included
1038 // files with files included by recursively calling itself
1039 if ($returnFiles && is_array($included_text)) {
1040 $includedFiles = array_merge($includedFiles, $included_text['files']);
1041 $included_text = $included_text['typoscript'];
1043 $content .= $included_text . LF
;
1045 $content .= '### @import \'' . $readableFileName . '\' end ###' . LF
. LF
;
1047 // load default TypoScript for content rendering templates like
1048 // fluid_styled_content if those have been included through e.g.
1049 // @import "fluid_styled_content/Configuration/TypoScript/setup.typoscript"
1050 if (strpos(strtoupper($filename), 'EXT:') === 0) {
1051 $filePointerPathParts = explode('/', substr($filename, 4));
1052 // remove file part, determine whether to load setup or constants
1053 list($includeType) = explode('.', array_pop($filePointerPathParts));
1055 if (in_array($includeType, ['setup', 'constants'], true
)) {
1056 // adapt extension key to required format (no underscores)
1057 $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
1059 // load default TypoScript
1060 $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
1061 if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true
)) {
1062 $content .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
1068 if (empty($content)) {
1069 return self
::typoscriptIncludeError('No file or folder found for importing TypoScript on "' . $filename . '".');
1075 * Include file $filename. Contents of the file will be prepended to &$newstring, filename to &$includedFiles
1076 * Further include_typoscript tags in the contents are processed recursively
1078 * @param string $filename Relative path to the typoscript file to be included
1079 * @param int $cycle_counter Counter for detecting endless loops
1080 * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1081 * @param string &$newString The output string to which the content of the file will be prepended (referenced
1082 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1083 * @param string $optionalProperties
1084 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1088 public static function includeFile($filename, $cycle_counter = 1, $returnFiles = false
, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1090 // Resolve a possible relative paths if a parent file is given
1091 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
1092 $absfilename = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
1094 $absfilename = $filename;
1096 $absfilename = GeneralUtility
::getFileAbsFileName($absfilename);
1098 $newString .= LF
. '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> BEGIN:' . LF
;
1099 if ((string)$filename !== '') {
1100 // Must exist and must not contain '..' and must be relative
1101 // Check for allowed files
1102 if (!GeneralUtility
::verifyFilenameAgainstDenyPattern($absfilename)) {
1103 $newString .= self
::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
1105 $fileExists = false
;
1106 if (@file_exists
($absfilename)) {
1111 $includedFiles[] = $absfilename;
1112 // check for includes in included text
1113 $included_text = self
::checkIncludeLines(file_get_contents($absfilename), $cycle_counter +
1, $returnFiles, $absfilename);
1114 // If the method also has to return all included files, merge currently included
1115 // files with files included by recursively calling itself
1116 if ($returnFiles && is_array($included_text)) {
1117 $includedFiles = array_merge($includedFiles, $included_text['files']);
1118 $included_text = $included_text['typoscript'];
1120 $newString .= $included_text . LF
;
1122 $newString .= self
::typoscriptIncludeError('File "' . $filename . '" was not found.');
1126 $newString .= '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> END:' . LF
. LF
;
1130 * Include all files with matching Typoscript extensions in directory $dirPath. Contents of the files are
1131 * prepended to &$newstring, filename to &$includedFiles.
1132 * Order of the directory items to be processed: files first, then directories, both in alphabetical order.
1133 * Further include_typoscript tags in the contents of the files are processed recursively.
1135 * @param string $dirPath Relative path to the directory to be included
1136 * @param int $cycle_counter Counter for detecting endless loops
1137 * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1138 * @param string &$newString The output string to which the content of the file will be prepended (referenced)
1139 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1140 * @param string $optionalProperties
1141 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1144 protected static function includeDirectory($dirPath, $cycle_counter = 1, $returnFiles = false
, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1146 // Extract the value of the property extensions="..."
1147 $matches = preg_split('#(?i)extensions\s*=\s*"([^"]*)"(\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE
);
1148 if (count($matches) > 1) {
1149 $includedFileExtensions = $matches[1];
1151 $includedFileExtensions = '';
1154 // Resolve a possible relative paths if a parent file is given
1155 if ($parentFilenameOrPath !== '' && $dirPath[0] === '.') {
1156 $resolvedDirPath = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $dirPath);
1158 $resolvedDirPath = $dirPath;
1160 $absDirPath = GeneralUtility
::getFileAbsFileName($resolvedDirPath);
1162 $absDirPath = rtrim($absDirPath, '/') . '/';
1163 $newString .= LF
. '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> BEGIN:' . LF
;
1164 // Get alphabetically sorted file index in array
1165 $fileIndex = GeneralUtility
::getAllFilesAndFoldersInPath([], $absDirPath, $includedFileExtensions);
1166 // Prepend file contents to $newString
1167 $prefixLength = strlen(Environment
::getPublicPath() . '/');
1168 foreach ($fileIndex as $absFileRef) {
1169 $relFileRef = substr($absFileRef, $prefixLength);
1170 self
::includeFile($relFileRef, $cycle_counter, $returnFiles, $newString, $includedFiles, '', $absDirPath);
1172 $newString .= '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> END:' . LF
. LF
;
1174 $newString .= self
::typoscriptIncludeError('The path "' . $resolvedDirPath . '" is invalid.');
1179 * Process errors in INCLUDE_TYPOSCRIPT tags
1180 * Errors are logged and printed in the concatenated TypoScript result (as can be seen in Template Analyzer)
1182 * @param string $error Text of the error message
1183 * @return string The error message encapsulated in comments
1186 protected static function typoscriptIncludeError($error)
1188 self
::getLogger()->warning($error);
1189 return "\n###\n### ERROR: " . $error . "\n###\n\n";
1193 * Parses the string in each value of the input array for include-commands
1195 * @param array $array Array with TypoScript in each value
1196 * @return array Same array but where the values has been parsed for include-commands
1198 public static function checkIncludeLines_array(array $array)
1200 foreach ($array as $k => $v) {
1201 $array[$k] = self
::checkIncludeLines($array[$k]);
1207 * Search for commented INCLUDE_TYPOSCRIPT statements
1208 * and save the content between the BEGIN and the END line to the specified file
1210 * @param string $string Template content
1211 * @param int $cycle_counter Counter for detecting endless loops
1212 * @param array $extractedFileNames
1213 * @param string $parentFilenameOrPath
1215 * @throws \RuntimeException
1216 * @throws \UnexpectedValueException
1217 * @return string Template content with uncommented include statements
1220 public static function extractIncludes($string, $cycle_counter = 1, array $extractedFileNames = [], $parentFilenameOrPath = '')
1222 if ($cycle_counter > 10) {
1223 self
::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
1226 ### ERROR: Recursion!
1230 $expectedEndTag = '';
1234 $inIncludePart = false
;
1235 $lines = preg_split("/\r\n|\n|\r/", $string);
1236 $skipNextLineIfEmpty = false
;
1237 $openingCommentedIncludeStatement = null
;
1238 $optionalProperties = '';
1239 foreach ($lines as $line) {
1240 // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1241 // an additional empty line, remove this again
1242 if ($skipNextLineIfEmpty) {
1243 if (trim($line) === '') {
1246 $skipNextLineIfEmpty = false
;
1249 // Outside commented include statements
1250 if (!$inIncludePart) {
1251 // Search for beginning commented include statements
1252 if (preg_match('/###\\s*<INCLUDE_TYPOSCRIPT:\\s*source\\s*=\\s*"\\s*((?i)file|dir)\\s*:\\s*([^"]*)"(.*)>\\s*BEGIN/i', $line, $matches)) {
1253 // Found a commented include statement
1255 // Save this line in case there is no ending tag
1256 $openingCommentedIncludeStatement = trim($line);
1257 $openingCommentedIncludeStatement = preg_replace('/\\s*### Warning: .*###\\s*/', '', $openingCommentedIncludeStatement);
1259 // type of match: FILE or DIR
1260 $inIncludePart = strtoupper($matches[1]);
1261 $fileName = $matches[2];
1262 $optionalProperties = $matches[3];
1264 $expectedEndTag = '### <INCLUDE_TYPOSCRIPT: source="' . $inIncludePart . ':' . $fileName . '"' . $optionalProperties . '> END';
1265 // Strip all whitespace characters to make comparison safer
1266 $expectedEndTag = strtolower(preg_replace('/\s/', '', $expectedEndTag));
1268 // If this is not a beginning commented include statement this line goes into the rest content
1269 $restContent[] = $line;
1272 // Inside commented include statements
1273 // Search for the matching ending commented include statement
1274 $strippedLine = preg_replace('/\s/', '', $line);
1275 if (stripos($strippedLine, $expectedEndTag) !== false
) {
1276 // Found the matching ending include statement
1277 $fileContentString = implode(PHP_EOL
, $fileContent);
1279 // Write the content to the file
1281 // Resolve a possible relative paths if a parent file is given
1282 if ($parentFilenameOrPath !== '' && $fileName[0] === '.') {
1283 $realFileName = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $fileName);
1285 $realFileName = $fileName;
1287 $realFileName = GeneralUtility
::getFileAbsFileName($realFileName);
1289 if ($inIncludePart === 'FILE') {
1291 if (!GeneralUtility
::verifyFilenameAgainstDenyPattern($realFileName)) {
1292 throw new \
UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
1294 if (empty($realFileName)) {
1295 throw new \
UnexpectedValueException(sprintf('"%s" is not a valid file location.', $fileName), 1294586441);
1297 if (!is_writable($realFileName)) {
1298 throw new \
RuntimeException(sprintf('"%s" is not writable.', $fileName), 1294586442);
1300 if (in_array($realFileName, $extractedFileNames)) {
1301 throw new \
RuntimeException(sprintf('Recursive/multiple inclusion of file "%s"', $realFileName), 1294586443);
1303 $extractedFileNames[] = $realFileName;
1305 // Recursive call to detected nested commented include statements
1306 $fileContentString = self
::extractIncludes($fileContentString, $cycle_counter +
1, $extractedFileNames, $realFileName);
1308 // Write the content to the file
1309 if (!GeneralUtility
::writeFile($realFileName, $fileContentString)) {
1310 throw new \
RuntimeException(sprintf('Could not write file "%s"', $realFileName), 1294586444);
1312 // Insert reference to the file in the rest content
1313 $restContent[] = '<INCLUDE_TYPOSCRIPT: source="FILE:' . $fileName . '"' . $optionalProperties . '>';
1318 if (empty($realFileName)) {
1319 throw new \
UnexpectedValueException(sprintf('"%s" is not a valid location.', $fileName), 1366493602);
1321 if (!is_dir($realFileName)) {
1322 throw new \
RuntimeException(sprintf('"%s" is not a directory.', $fileName), 1366493603);
1324 if (in_array($realFileName, $extractedFileNames)) {
1325 throw new \
RuntimeException(sprintf('Recursive/multiple inclusion of directory "%s"', $realFileName), 1366493604);
1327 $extractedFileNames[] = $realFileName;
1329 // Recursive call to detected nested commented include statements
1330 self
::extractIncludes($fileContentString, $cycle_counter +
1, $extractedFileNames, $realFileName);
1332 // just drop content between tags since it should usually just contain individual files from that dir
1334 // Insert reference to the dir in the rest content
1335 $restContent[] = '<INCLUDE_TYPOSCRIPT: source="DIR:' . $fileName . '"' . $optionalProperties . '>';
1338 // Reset variables (preparing for the next commented include statement)
1341 $inIncludePart = false
;
1342 $openingCommentedIncludeStatement = null
;
1343 // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1344 // an additional empty line, remove this again
1345 $skipNextLineIfEmpty = true
;
1347 // If this is not an ending commented include statement this line goes into the file content
1348 $fileContent[] = $line;
1352 // If we're still inside commented include statements copy the lines back to the rest content
1353 if ($inIncludePart) {
1354 $restContent[] = $openingCommentedIncludeStatement . ' ### Warning: Corresponding end line missing! ###';
1355 $restContent = array_merge($restContent, $fileContent);
1357 $restContentString = implode(PHP_EOL
, $restContent);
1358 return $restContentString;
1362 * Processes the string in each value of the input array with extractIncludes
1364 * @param array $array Array with TypoScript in each value
1365 * @return array Same array but where the values has been processed with extractIncludes
1367 public static function extractIncludes_array(array $array)
1369 foreach ($array as $k => $v) {
1370 $array[$k] = self
::extractIncludes($array[$k]);
1375 /**********************************
1377 * Syntax highlighting
1379 *********************************/
1381 * Syntax highlight a TypoScript text
1382 * Will parse the content. Remember, the internal setup array may contain invalid parsed content since conditions are ignored!
1384 * @param string $string The TypoScript text
1385 * @param mixed $lineNum If blank, linenumbers are NOT printed. If array then the first key is the linenumber offset to add to the internal counter.
1386 * @param bool $highlightBlockMode If set, then the highlighted output will be formatted in blocks based on the brace levels. prespace will be ignored and empty lines represented with a single no-break-space.
1387 * @return string HTML code for the syntax highlighted string
1389 public function doSyntaxHighlight($string, $lineNum = '', $highlightBlockMode = false
)
1391 $this->syntaxHighLight
= true
;
1392 $this->highLightData
= [];
1394 // This is done in order to prevent empty <span>..</span> sections around CR content. Should not do anything but help lessen the amount of HTML code.
1395 $string = str_replace(CR
, '', $string);
1396 $this->parse($string);
1397 return $this->syntaxHighlight_print($lineNum, $highlightBlockMode);
1401 * Registers a part of a TypoScript line for syntax highlighting.
1403 * @param string $code Key from the internal array $this->highLightStyles
1404 * @param int $pointer Pointer to the line in $this->raw which this is about
1405 * @param int $strlen The number of chars LEFT on this line before the end is reached.
1408 protected function regHighLight($code, $pointer, $strlen = -1)
1410 if ($strlen === -1) {
1411 $this->highLightData
[$pointer] = [[$code, 0]];
1413 $this->highLightData
[$pointer][] = [$code, $strlen];
1415 $this->highLightData_bracelevel
[$pointer] = $this->inBrace
;
1419 * Formatting the TypoScript code in $this->raw based on the data collected by $this->regHighLight in $this->highLightData
1421 * @param mixed $lineNumDat If blank, linenumbers are NOT printed. If array then the first key is the linenumber offset to add to the internal counter.
1422 * @param bool $highlightBlockMode If set, then the highlighted output will be formatted in blocks based on the brace levels. prespace will be ignored and empty lines represented with a single no-break-space.
1423 * @return string HTML content
1424 * @see doSyntaxHighlight()
1426 protected function syntaxHighlight_print($lineNumDat, $highlightBlockMode)
1428 // Registers all error messages in relation to their linenumber
1430 foreach ($this->errors
as $err) {
1431 $errA[$err[2]][] = $err[0];
1433 // Generates the syntax highlighted output:
1435 foreach ($this->raw
as $rawP => $value) {
1437 $strlen = strlen($value);
1439 if (is_array($this->highLightData
[$rawP])) {
1440 foreach ($this->highLightData
[$rawP] as $set) {
1441 $len = $strlen - $start - $set[1];
1443 $part = substr($value, $start, $len);
1445 $st = $this->highLightStyles
[isset($this->highLightStyles
[$set[0]]) ?
$set[0] : 'default'];
1446 if (!$highlightBlockMode ||
$set[0] !== 'prespace') {
1447 $lineC .= $st[0] . htmlspecialchars($part) . $st[1];
1449 } elseif ($len < 0) {
1450 debug([$len, $value, $rawP]);
1456 if (strlen($value) > $start) {
1457 $lineC .= $this->highLightStyles
['ignored'][0] . htmlspecialchars(substr($value, $start)) . $this->highLightStyles
['ignored'][1];
1460 $lineC .= $this->highLightStyles
['error'][0] . '<strong> - ERROR:</strong> ' . htmlspecialchars(implode(';', $errA[$rawP])) . $this->highLightStyles
['error'][1];
1462 if ($highlightBlockMode && $this->highLightData_bracelevel
[$rawP]) {
1463 $lineC = str_pad('', $this->highLightData_bracelevel
[$rawP] * 2, ' ', STR_PAD_LEFT
) . '<span style="' . $this->highLightBlockStyles
. ($this->highLightBlockStyles_basecolor ?
'background-color: ' . $this->modifyHTMLColorAll($this->highLightBlockStyles_basecolor
, -$this->highLightData_bracelevel
[$rawP] * 16) : '') . '">' . ($lineC !== '' ?
$lineC : ' ') . '</span>';
1465 if (is_array($lineNumDat)) {
1466 $lineNum = $rawP +
$lineNumDat[0];
1467 if ($this->parentObject
instanceof ExtendedTemplateService
) {
1468 $lineNum = $this->parentObject
->ext_lnBreakPointWrap($lineNum, $lineNum);
1470 $lineC = $this->highLightStyles
['linenum'][0] . str_pad($lineNum, 4, ' ', STR_PAD_LEFT
) . ':' . $this->highLightStyles
['linenum'][1] . ' ' . $lineC;
1474 return '<pre class="ts-hl">' . implode(LF
, $lines) . '</pre>';
1478 * @return TimeTracker
1480 protected function getTimeTracker()
1482 return GeneralUtility
::makeInstance(TimeTracker
::class);
1486 * Modifies a HTML Hex color by adding/subtracting $R,$G and $B integers
1488 * @param string $color A hexadecimal color code, #xxxxxx
1489 * @param int $R Offset value 0-255
1490 * @param int $G Offset value 0-255
1491 * @param int $B Offset value 0-255
1492 * @return string A hexadecimal color code, #xxxxxx, modified according to input vars
1493 * @see modifyHTMLColorAll()
1495 protected function modifyHTMLColor($color, $R, $G, $B)
1497 // This takes a hex-color (# included!) and adds $R, $G and $B to the HTML-color (format: #xxxxxx) and returns the new color
1498 $nR = MathUtility
::forceIntegerInRange(hexdec(substr($color, 1, 2)) +
$R, 0, 255);
1499 $nG = MathUtility
::forceIntegerInRange(hexdec(substr($color, 3, 2)) +
$G, 0, 255);
1500 $nB = MathUtility
::forceIntegerInRange(hexdec(substr($color, 5, 2)) +
$B, 0, 255);
1501 return '#' . substr('0' . dechex($nR), -2) . substr('0' . dechex($nG), -2) . substr('0' . dechex($nB), -2);
1505 * Modifies a HTML Hex color by adding/subtracting $all integer from all R/G/B channels
1507 * @param string $color A hexadecimal color code, #xxxxxx
1508 * @param int $all Offset value 0-255 for all three channels.
1509 * @return string A hexadecimal color code, #xxxxxx, modified according to input vars
1510 * @see modifyHTMLColor()
1512 protected function modifyHTMLColorAll($color, $all)
1514 return $this->modifyHTMLColor($color, $all, $all, $all);
1518 * Get a logger instance
1520 * This class uses logging mostly in static functions, hence we need a static getter for the logger.
1521 * Injection of a logger instance via GeneralUtility::makeInstance is not possible.
1523 * @return LoggerInterface
1525 protected static function getLogger()
1527 return GeneralUtility
::makeInstance(LogManager
::class)->getLogger(__CLASS__
);