e4d0d0bbb9949af3e293b0f0d1be2ce2eafefa73
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / TypoScript / Parser / TypoScriptParser.php
1 <?php
2 namespace TYPO3\CMS\Core\TypoScript\Parser;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
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.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
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\Compatibility\PublicMethodDeprecationTrait;
21 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
22 use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
23 use TYPO3\CMS\Core\Core\Environment;
24 use TYPO3\CMS\Core\Log\LogManager;
25 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
26 use TYPO3\CMS\Core\TypoScript\ExtendedTemplateService;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\MathUtility;
29 use TYPO3\CMS\Core\Utility\PathUtility;
30 use TYPO3\CMS\Core\Utility\StringUtility;
31 use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher;
32
33 /**
34 * The TypoScript parser
35 */
36 class TypoScriptParser
37 {
38 use PublicPropertyDeprecationTrait;
39 use PublicMethodDeprecationTrait;
40
41 protected $deprecatedPublicProperties = [
42 'raw' => 'Using $raw of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
43 'rawP' => 'Using $rawP of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
44 'lastComment' => 'Using $lastComment of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
45 'commentSet' => 'Using $commentSet of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
46 'multiLineEnabled' => 'Using $multiLineEnabled of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
47 'multiLineObject' => 'Using $multiLineObject of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
48 'multiLineValue' => 'Using $multiLineValue of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
49 'inBrace' => 'Using $inBrace of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
50 'lastConditionTrue' => 'Using $lastConditionTrue of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
51 'syntaxHighLight' => 'Using $syntaxHighLight of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
52 'highLightData' => 'Using $highLightData of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
53 'highLightData_bracelevel' => 'Using $highLightData_bracelevel of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
54 'highLightStyles' => 'Using $highLightStyles of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
55 'highLightBlockStyles' => 'Using $highLightBlockStyles of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
56 'highLightBlockStyles_basecolor' => 'Using $highLightBlockStyles_basecolor of class TypoScriptParser from the outside is discouraged, as this variable is only used for internal storage.',
57 ];
58
59 protected $deprecatedPublicMethods = [
60 'nextDivider' => 'Using nextDivider() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
61 'parseSub' => 'Using parseSub() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
62 'rollParseSub' => 'Using rollParseSub() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
63 'setVal' => 'Using setVal() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
64 'error' => 'Using error() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
65 'regHighLight' => 'Using regHighLight() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
66 'syntaxHighlight_print' => 'Using syntaxHighlight_print() of class TypoScriptParser from the outside is discouraged, as this method is only meant to be used internally.',
67 ];
68
69 /**
70 * TypoScript hierarchy being build during parsing.
71 *
72 * @var array
73 */
74 public $setup = [];
75
76 /**
77 * Raw data, the input string exploded by LF
78 *
79 * @var array
80 */
81 protected $raw;
82
83 /**
84 * Pointer to entry in raw data array
85 *
86 * @var int
87 */
88 protected $rawP;
89
90 /**
91 * Holding the value of the last comment
92 *
93 * @var string
94 */
95 protected $lastComment = '';
96
97 /**
98 * Internally set, used as internal flag to create a multi-line comment (one of those like /* ... * /
99 *
100 * @var bool
101 */
102 protected $commentSet = false;
103
104 /**
105 * Internally set, when multiline value is accumulated
106 *
107 * @var bool
108 */
109 protected $multiLineEnabled = false;
110
111 /**
112 * Internally set, when multiline value is accumulated
113 *
114 * @var string
115 */
116 protected $multiLineObject = '';
117
118 /**
119 * Internally set, when multiline value is accumulated
120 *
121 * @var array
122 */
123 protected $multiLineValue = [];
124
125 /**
126 * Internally set, when in brace. Counter.
127 *
128 * @var int
129 */
130 protected $inBrace = 0;
131
132 /**
133 * For each condition this flag is set, if the condition is TRUE,
134 * else it's cleared. Then it's used by the [ELSE] condition to determine if the next part should be parsed.
135 *
136 * @var bool
137 */
138 protected $lastConditionTrue = true;
139
140 /**
141 * Tracking all conditions found
142 *
143 * @var array
144 */
145 public $sections = [];
146
147 /**
148 * Tracking all matching conditions found
149 *
150 * @var array
151 */
152 public $sectionsMatch = [];
153
154 /**
155 * If set, then syntax highlight mode is on; Call the function syntaxHighlight() to use this function
156 *
157 * @var bool
158 */
159 protected $syntaxHighLight = false;
160
161 /**
162 * Syntax highlight data is accumulated in this array. Used by syntaxHighlight_print() to construct the output.
163 *
164 * @var array
165 */
166 protected $highLightData = [];
167
168 /**
169 * Syntax highlight data keeping track of the curly brace level for each line
170 *
171 * @var array
172 */
173 protected $highLightData_bracelevel = [];
174
175 /**
176 * DO NOT register the comments. This is default for the ordinary sitetemplate!
177 *
178 * @var bool
179 */
180 public $regComments = false;
181
182 /**
183 * DO NOT register the linenumbers. This is default for the ordinary sitetemplate!
184 *
185 * @var bool
186 */
187 public $regLinenumbers = false;
188
189 /**
190 * Error accumulation array.
191 *
192 * @var array
193 */
194 public $errors = [];
195
196 /**
197 * Used for the error messages line number reporting. Set externally.
198 *
199 * @var int
200 */
201 public $lineNumberOffset = 0;
202
203 /**
204 * Line for break point.
205 *
206 * @var int
207 */
208 public $breakPointLN = 0;
209
210 /**
211 * @var array
212 */
213 protected $highLightStyles = [
214 'prespace' => ['<span class="ts-prespace">', '</span>'],
215 // Space before any content on a line
216 'objstr_postspace' => ['<span class="ts-objstr_postspace">', '</span>'],
217 // Space after the object string on a line
218 'operator_postspace' => ['<span class="ts-operator_postspace">', '</span>'],
219 // Space after the operator on a line
220 'operator' => ['<span class="ts-operator">', '</span>'],
221 // The operator char
222 'value' => ['<span class="ts-value">', '</span>'],
223 // The value of a line
224 'objstr' => ['<span class="ts-objstr">', '</span>'],
225 // The object string of a line
226 'value_copy' => ['<span class="ts-value_copy">', '</span>'],
227 // The value when the copy syntax (<) is used; that means the object reference
228 'value_unset' => ['<span class="ts-value_unset">', '</span>'],
229 // The value when an object is unset. Should not exist.
230 'ignored' => ['<span class="ts-ignored">', '</span>'],
231 // The "rest" of a line which will be ignored.
232 'default' => ['<span class="ts-default">', '</span>'],
233 // The default style if none other is applied.
234 'comment' => ['<span class="ts-comment">', '</span>'],
235 // Comment lines
236 'condition' => ['<span class="ts-condition">', '</span>'],
237 // Conditions
238 'error' => ['<span class="ts-error">', '</span>'],
239 // Error messages
240 'linenum' => ['<span class="ts-linenum">', '</span>']
241 ];
242
243 /**
244 * Additional attributes for the <span> tags for a blockmode line
245 *
246 * @var string
247 */
248 protected $highLightBlockStyles = '';
249
250 /**
251 * The hex-HTML color for the blockmode
252 *
253 * @var string
254 */
255 protected $highLightBlockStyles_basecolor = '#cccccc';
256
257 /**
258 * @var \TYPO3\CMS\Core\TypoScript\ExtendedTemplateService
259 */
260 public $parentObject;
261
262 /**
263 * Start parsing the input TypoScript text piece. The result is stored in $this->setup
264 *
265 * @param string $string The TypoScript text
266 * @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])
267 */
268 public function parse($string, $matchObj = '')
269 {
270 $this->raw = explode(LF, $string);
271 $this->rawP = 0;
272 $pre = '[GLOBAL]';
273 while ($pre) {
274 if ($this->breakPointLN && $pre === '[_BREAK]') {
275 $this->error('Breakpoint at ' . ($this->lineNumberOffset + $this->rawP - 2) . ': Line content was "' . $this->raw[$this->rawP - 2] . '"', 1);
276 break;
277 }
278 if ($pre === '[]') {
279 $this->error('Empty condition is always false, this does not make sense. At line ' . ($this->lineNumberOffset + $this->rawP - 1), 2);
280 break;
281 }
282 $preUppercase = strtoupper($pre);
283 if ($pre[0] === '[' &&
284 ($preUppercase === '[GLOBAL]' ||
285 $preUppercase === '[END]' ||
286 !$this->lastConditionTrue && $preUppercase === '[ELSE]')
287 ) {
288 $pre = trim($this->parseSub($this->setup));
289 $this->lastConditionTrue = true;
290 } else {
291 // We're in a specific section. Therefore we log this section
292 $specificSection = $preUppercase !== '[ELSE]';
293 if ($specificSection) {
294 $this->sections[md5($pre)] = $pre;
295 }
296 if (is_object($matchObj) && $matchObj->match($pre) || $this->syntaxHighLight) {
297 if ($specificSection) {
298 $this->sectionsMatch[md5($pre)] = $pre;
299 }
300 $pre = trim($this->parseSub($this->setup));
301 $this->lastConditionTrue = true;
302 } else {
303 $pre = $this->nextDivider();
304 $this->lastConditionTrue = false;
305 }
306 }
307 }
308 if ($this->inBrace) {
309 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)', 1);
310 }
311 if ($this->multiLineEnabled) {
312 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': A multiline value section is not ended with a parenthesis!', 1);
313 }
314 $this->lineNumberOffset += count($this->raw) + 1;
315 }
316
317 /**
318 * 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.
319 *
320 * @return string The condition value
321 * @see parse()
322 */
323 protected function nextDivider()
324 {
325 while (isset($this->raw[$this->rawP])) {
326 $line = trim($this->raw[$this->rawP]);
327 $this->rawP++;
328 if ($line && $line[0] === '[') {
329 return $line;
330 }
331 }
332 return '';
333 }
334
335 /**
336 * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
337 *
338 * @param array $setup Reference to the setup array in which to accumulate the values.
339 * @return string|null Returns the string of the condition found, the exit signal or possible nothing (if it completed parsing with no interruptions)
340 */
341 protected function parseSub(array &$setup)
342 {
343 while (isset($this->raw[$this->rawP])) {
344 $line = ltrim($this->raw[$this->rawP]);
345 $lineP = $this->rawP;
346 $this->rawP++;
347 if ($this->syntaxHighLight) {
348 $this->regHighLight('prespace', $lineP, strlen($line));
349 }
350 // Breakpoint?
351 // By adding 1 we get that line processed
352 if ($this->breakPointLN && $this->lineNumberOffset + $this->rawP - 1 === $this->breakPointLN + 1) {
353 return '[_BREAK]';
354 }
355 // Set comment flag?
356 if (!$this->multiLineEnabled && strpos($line, '/*') === 0) {
357 $this->commentSet = true;
358 }
359 // If $this->multiLineEnabled we will go and get the line values here because we know, the first if() will be TRUE.
360 if (!$this->commentSet && ($line || $this->multiLineEnabled)) {
361 // If multiline is enabled. Escape by ')'
362 if ($this->multiLineEnabled) {
363 // Multiline ends...
364 if (!empty($line[0]) && $line[0] === ')') {
365 if ($this->syntaxHighLight) {
366 $this->regHighLight('operator', $lineP, strlen($line) - 1);
367 }
368 // Disable multiline
369 $this->multiLineEnabled = false;
370 $theValue = implode($this->multiLineValue, LF);
371 if (strpos($this->multiLineObject, '.') !== false) {
372 // Set the value deeper.
373 $this->setVal($this->multiLineObject, $setup, [$theValue]);
374 } else {
375 // Set value regularly
376 $setup[$this->multiLineObject] = $theValue;
377 if ($this->lastComment && $this->regComments) {
378 $setup[$this->multiLineObject . '..'] .= $this->lastComment;
379 }
380 if ($this->regLinenumbers) {
381 $setup[$this->multiLineObject . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
382 }
383 }
384 } else {
385 if ($this->syntaxHighLight) {
386 $this->regHighLight('value', $lineP);
387 }
388 $this->multiLineValue[] = $this->raw[$this->rawP - 1];
389 }
390 } elseif ($this->inBrace === 0 && $line[0] === '[') {
391 // Beginning of condition (only on level zero compared to brace-levels
392 if ($this->syntaxHighLight) {
393 $this->regHighLight('condition', $lineP);
394 }
395 return $line;
396 } else {
397 // Return if GLOBAL condition is set - no matter what.
398 if ($line[0] === '[' && stripos($line, '[GLOBAL]') !== false) {
399 if ($this->syntaxHighLight) {
400 $this->regHighLight('condition', $lineP);
401 }
402 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': On return to [GLOBAL] scope, the script was short of ' . $this->inBrace . ' end brace(s)', 1);
403 $this->inBrace = 0;
404 return $line;
405 }
406 if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
407 // If not brace-end or comment
408 // Find object name string until we meet an operator
409 $varL = strcspn($line, "\t" . ' {=<>(');
410 // check for special ":=" operator
411 if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
412 --$varL;
413 }
414 // also remove tabs after the object string name
415 $objStrName = substr($line, 0, $varL);
416 if ($this->syntaxHighLight) {
417 $this->regHighLight('objstr', $lineP, strlen(substr($line, $varL)));
418 }
419 if ($objStrName !== '') {
420 $r = [];
421 if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
422 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."');
423 } else {
424 $line = ltrim(substr($line, $varL));
425 if ($this->syntaxHighLight) {
426 $this->regHighLight('objstr_postspace', $lineP, strlen($line));
427 if ($line !== '') {
428 $this->regHighLight('operator', $lineP, strlen($line) - 1);
429 $this->regHighLight('operator_postspace', $lineP, strlen(ltrim(substr($line, 1))));
430 }
431 }
432 if ($line === '') {
433 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
434 } else {
435 // Checking for special TSparser properties (to change TS values at parsetime)
436 $match = [];
437 if ($line[0] === ':' && preg_match('/^:=\\s*([[:alpha:]]+)\\s*\\((.*)\\).*/', $line, $match)) {
438 $tsFunc = $match[1];
439 $tsFuncArg = $match[2];
440 $val = $this->getVal($objStrName, $setup);
441 $currentValue = $val[0] ?? null;
442 $tsFuncArg = str_replace(['\\\\', '\\n', '\\t'], ['\\', LF, "\t"], $tsFuncArg);
443 $newValue = $this->executeValueModifier($tsFunc, $tsFuncArg, $currentValue);
444 if (isset($newValue)) {
445 $line = '= ' . $newValue;
446 }
447 }
448 switch ($line[0]) {
449 case '=':
450 if ($this->syntaxHighLight) {
451 $this->regHighLight('value', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
452 }
453 if (strpos($objStrName, '.') !== false) {
454 $value = [];
455 $value[0] = trim(substr($line, 1));
456 $this->setVal($objStrName, $setup, $value);
457 } else {
458 $setup[$objStrName] = trim(substr($line, 1));
459 if ($this->lastComment && $this->regComments) {
460 // Setting comment..
461 $setup[$objStrName . '..'] .= $this->lastComment;
462 }
463 if ($this->regLinenumbers) {
464 $setup[$objStrName . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
465 }
466 }
467 break;
468 case '{':
469 $this->inBrace++;
470 if (strpos($objStrName, '.') !== false) {
471 $exitSig = $this->rollParseSub($objStrName, $setup);
472 if ($exitSig) {
473 return $exitSig;
474 }
475 } else {
476 if (!isset($setup[$objStrName . '.'])) {
477 $setup[$objStrName . '.'] = [];
478 }
479 $exitSig = $this->parseSub($setup[$objStrName . '.']);
480 if ($exitSig) {
481 return $exitSig;
482 }
483 }
484 break;
485 case '(':
486 $this->multiLineObject = $objStrName;
487 $this->multiLineEnabled = true;
488 $this->multiLineValue = [];
489 break;
490 case '<':
491 if ($this->syntaxHighLight) {
492 $this->regHighLight('value_copy', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
493 }
494 $theVal = trim(substr($line, 1));
495 if ($theVal[0] === '.') {
496 $res = $this->getVal(substr($theVal, 1), $setup);
497 } else {
498 $res = $this->getVal($theVal, $this->setup);
499 }
500 // unserialize(serialize(...)) may look stupid but is needed because of some reference issues.
501 // See forge issue #76919 and functional test hasFlakyReferences()
502 $this->setVal($objStrName, $setup, unserialize(serialize($res)), 1);
503 break;
504 case '>':
505 if ($this->syntaxHighLight) {
506 $this->regHighLight('value_unset', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
507 }
508 $this->setVal($objStrName, $setup, 'UNSET');
509 break;
510 default:
511 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
512 }
513 }
514 }
515 $this->lastComment = '';
516 }
517 } elseif ($line[0] === '}') {
518 $this->inBrace--;
519 $this->lastComment = '';
520 if ($this->syntaxHighLight) {
521 $this->regHighLight('operator', $lineP, strlen($line) - 1);
522 }
523 if ($this->inBrace < 0) {
524 $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': An end brace is in excess.', 1);
525 $this->inBrace = 0;
526 } else {
527 break;
528 }
529 } else {
530 if ($this->syntaxHighLight) {
531 $this->regHighLight('comment', $lineP);
532 }
533 // Comment. The comments are concatenated in this temporary string:
534 if ($this->regComments) {
535 $this->lastComment .= rtrim($line) . LF;
536 }
537 }
538 if (strpos($line, '### ERROR') === 0) {
539 $this->error(substr($line, 11));
540 }
541 }
542 }
543 // Unset comment
544 if ($this->commentSet) {
545 if ($this->syntaxHighLight) {
546 $this->regHighLight('comment', $lineP);
547 }
548 if (strpos($line, '*/') !== false) {
549 $this->commentSet = false;
550 }
551 }
552 }
553 return null;
554 }
555
556 /**
557 * Executes operator functions, called from TypoScript
558 * example: page.10.value := appendString(!)
559 *
560 * @param string $modifierName TypoScript function called
561 * @param string $modifierArgument Function arguments; In case of multiple arguments, the method must split on its own
562 * @param string $currentValue Current TypoScript value
563 * @return string|null Modified result or null for no modification
564 */
565 protected function executeValueModifier($modifierName, $modifierArgument = null, $currentValue = null)
566 {
567 $newValue = null;
568 switch ($modifierName) {
569 case 'prependString':
570 $newValue = $modifierArgument . $currentValue;
571 break;
572 case 'appendString':
573 $newValue = $currentValue . $modifierArgument;
574 break;
575 case 'removeString':
576 $newValue = str_replace($modifierArgument, '', $currentValue);
577 break;
578 case 'replaceString':
579 $modifierArgumentArray = explode('|', $modifierArgument, 2);
580 $fromStr = $modifierArgumentArray[0] ?? '';
581 $toStr = $modifierArgumentArray[1] ?? '';
582 $newValue = str_replace($fromStr, $toStr, $currentValue);
583 break;
584 case 'addToList':
585 $newValue = ((string)$currentValue !== '' ? $currentValue . ',' : '') . $modifierArgument;
586 break;
587 case 'removeFromList':
588 $existingElements = GeneralUtility::trimExplode(',', $currentValue);
589 $removeElements = GeneralUtility::trimExplode(',', $modifierArgument);
590 if (!empty($removeElements)) {
591 $newValue = implode(',', array_diff($existingElements, $removeElements));
592 }
593 break;
594 case 'uniqueList':
595 $elements = GeneralUtility::trimExplode(',', $currentValue);
596 $newValue = implode(',', array_unique($elements));
597 break;
598 case 'reverseList':
599 $elements = GeneralUtility::trimExplode(',', $currentValue);
600 $newValue = implode(',', array_reverse($elements));
601 break;
602 case 'sortList':
603 $elements = GeneralUtility::trimExplode(',', $currentValue);
604 $arguments = GeneralUtility::trimExplode(',', $modifierArgument);
605 $arguments = array_map('strtolower', $arguments);
606 $sort_flags = SORT_REGULAR;
607 if (in_array('numeric', $arguments)) {
608 $sort_flags = SORT_NUMERIC;
609 // If the sorting modifier "numeric" is given, all values
610 // are checked and an exception is thrown if a non-numeric value is given
611 // otherwise there is a different behaviour between PHP7 and PHP 5.x
612 // See also the warning on http://us.php.net/manual/en/function.sort.php
613 foreach ($elements as $element) {
614 if (!is_numeric($element)) {
615 throw new \InvalidArgumentException('The list "' . $currentValue . '" should be sorted numerically but contains a non-numeric value', 1438191758);
616 }
617 }
618 }
619 sort($elements, $sort_flags);
620 if (in_array('descending', $arguments)) {
621 $elements = array_reverse($elements);
622 }
623 $newValue = implode(',', $elements);
624 break;
625 case 'getEnv':
626 $environmentValue = getenv(trim($modifierArgument));
627 if ($environmentValue !== false) {
628 $newValue = $environmentValue;
629 }
630 break;
631 default:
632 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName])) {
633 $hookMethod = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName];
634 $params = ['currentValue' => $currentValue, 'functionArgument' => $modifierArgument];
635 $fakeThis = false;
636 $newValue = GeneralUtility::callUserFunction($hookMethod, $params, $fakeThis);
637 } else {
638 self::getLogger()->warning('Missing function definition for ' . $modifierName . ' on TypoScript');
639 }
640 }
641 return $newValue;
642 }
643
644 /**
645 * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
646 * thus having to recursively call itself to get the value
647 *
648 * @param string $string The object sub-path, eg "thisprop.another_prot
649 * @param array $setup The local setup array from the function calling this function
650 * @return string Returns the exitSignal
651 * @see parseSub()
652 */
653 protected function rollParseSub($string, array &$setup)
654 {
655 if ((string)$string === '') {
656 return '';
657 }
658
659 list($key, $remainingKey) = $this->parseNextKeySegment($string);
660 $key .= '.';
661 if (!isset($setup[$key])) {
662 $setup[$key] = [];
663 }
664 $exitSig = $remainingKey === ''
665 ? $this->parseSub($setup[$key])
666 : $this->rollParseSub($remainingKey, $setup[$key]);
667 return $exitSig ?: '';
668 }
669
670 /**
671 * Get a value/property pair for an object path in TypoScript, eg. "myobject.myvalue.mysubproperty".
672 * Here: Used by the "copy" operator, <
673 *
674 * @param string $string Object path for which to get the value
675 * @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.
676 * @return array An array with keys 0/1 being value/property respectively
677 */
678 public function getVal($string, $setup)
679 {
680 if ((string)$string === '') {
681 return [];
682 }
683
684 list($key, $remainingKey) = $this->parseNextKeySegment($string);
685 $subKey = $key . '.';
686 if ($remainingKey === '') {
687 $retArr = [];
688 if (isset($setup[$key])) {
689 $retArr[0] = $setup[$key];
690 }
691 if (isset($setup[$subKey])) {
692 $retArr[1] = $setup[$subKey];
693 }
694 return $retArr;
695 }
696 if ($setup[$subKey]) {
697 return $this->getVal($remainingKey, $setup[$subKey]);
698 }
699
700 return [];
701 }
702
703 /**
704 * Setting a value/property of an object string in the setup array.
705 *
706 * @param string $string The object sub-path, eg "thisprop.another_prot
707 * @param array $setup The local setup array from the function calling this function.
708 * @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)
709 * @param bool $wipeOut If set, then both value and property is wiped out when a copy is made of another value.
710 */
711 protected function setVal($string, array &$setup, $value, $wipeOut = false)
712 {
713 if ((string)$string === '') {
714 return;
715 }
716
717 list($key, $remainingKey) = $this->parseNextKeySegment($string);
718 $subKey = $key . '.';
719 if ($remainingKey === '') {
720 if ($value === 'UNSET') {
721 unset($setup[$key]);
722 unset($setup[$subKey]);
723 if ($this->regLinenumbers) {
724 $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '>';
725 }
726 } else {
727 $lnRegisDone = 0;
728 if ($wipeOut) {
729 unset($setup[$key]);
730 unset($setup[$subKey]);
731 if ($this->regLinenumbers) {
732 $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '<';
733 $lnRegisDone = 1;
734 }
735 }
736 if (isset($value[0])) {
737 $setup[$key] = $value[0];
738 }
739 if (isset($value[1])) {
740 $setup[$subKey] = $value[1];
741 }
742 if ($this->lastComment && $this->regComments) {
743 $setup[$key . '..'] .= $this->lastComment;
744 }
745 if ($this->regLinenumbers && !$lnRegisDone) {
746 $setup[$key . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
747 }
748 }
749 } else {
750 if (!isset($setup[$subKey])) {
751 $setup[$subKey] = [];
752 }
753 $this->setVal($remainingKey, $setup[$subKey], $value);
754 }
755 }
756
757 /**
758 * Determines the first key segment of a TypoScript key by searching for the first
759 * unescaped dot in the given key string.
760 *
761 * Since the escape characters are only needed to correctly determine the key
762 * segment any escape characters before the first unescaped dot are
763 * stripped from the key.
764 *
765 * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
766 * @return array Array with key segment and remaining part of $key
767 */
768 protected function parseNextKeySegment($key)
769 {
770 // if no dot is in the key, nothing to do
771 $dotPosition = strpos($key, '.');
772 if ($dotPosition === false) {
773 return [$key, ''];
774 }
775
776 if (strpos($key, '\\') !== false) {
777 // backslashes are in the key, so we do further parsing
778
779 while ($dotPosition !== false) {
780 if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
781 break;
782 }
783 // escaped dot found, continue
784 $dotPosition = strpos($key, '.', $dotPosition + 1);
785 }
786
787 if ($dotPosition === false) {
788 // no regular dot found
789 $keySegment = $key;
790 $remainingKey = '';
791 } else {
792 if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
793 $keySegment = substr($key, 0, $dotPosition - 1);
794 } else {
795 $keySegment = substr($key, 0, $dotPosition);
796 }
797 $remainingKey = substr($key, $dotPosition + 1);
798 }
799
800 // fix key segment by removing escape sequences
801 $keySegment = str_replace('\\.', '.', $keySegment);
802 } else {
803 // no backslash in the key, we're fine off
804 list($keySegment, $remainingKey) = explode('.', $key, 2);
805 }
806 return [$keySegment, $remainingKey];
807 }
808
809 /**
810 * Stacks errors/messages from the TypoScript parser into an internal array, $this->error
811 * 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.
812 *
813 * @param string $err The error message string
814 * @param int $num The error severity (in the scale of TimeTracker::setTSlogMessage: Approx: 2=warning, 1=info, 0=nothing, 3=fatal.)
815 */
816 protected function error($err, $num = 2)
817 {
818 $tt = $this->getTimeTracker();
819 if ($tt !== null) {
820 $tt->setTSlogMessage($err, $num);
821 }
822 $this->errors[] = [$err, $num, $this->rawP - 1, $this->lineNumberOffset];
823 }
824
825 /**
826 * Checks the input string (un-parsed TypoScript) for include-commands ("<INCLUDE_TYPOSCRIPT: ....")
827 * Use: \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines()
828 *
829 * @param string $string Unparsed TypoScript
830 * @param int $cycle_counter Counter for detecting endless loops
831 * @param bool $returnFiles When set an array containing the resulting typoscript and all included files will get returned
832 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
833 * @return string|array Complete TypoScript with includes added.
834 * @static
835 */
836 public static function checkIncludeLines($string, $cycle_counter = 1, $returnFiles = false, $parentFilenameOrPath = '')
837 {
838 $includedFiles = [];
839 if ($cycle_counter > 100) {
840 self::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
841 if ($returnFiles) {
842 return [
843 'typoscript' => '',
844 'files' => $includedFiles
845 ];
846 }
847 return '
848 ###
849 ### ERROR: Recursion!
850 ###
851 ';
852 }
853
854 if ($string !== null) {
855 $string = StringUtility::removeByteOrderMark($string);
856 }
857
858 // Checking for @import syntax imported files
859 $string = self::addImportsFromExternalFiles($string, $cycle_counter, $returnFiles, $includedFiles, $parentFilenameOrPath);
860
861 // If no tags found, no need to do slower preg_split
862 if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== false) {
863 $splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
864 $parts = preg_split($splitRegEx, LF . $string . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
865 // First text part goes through
866 $newString = $parts[0] . LF;
867 $partCount = count($parts);
868 for ($i = 1; $i + 3 < $partCount; $i += 4) {
869 // $parts[$i] contains 'FILE' or 'DIR'
870 // $parts[$i+1] contains relative file or directory path to be included
871 // $parts[$i+2] optional properties of the INCLUDE statement
872 // $parts[$i+3] next part of the typoscript string (part in between include-tags)
873 $includeType = $parts[$i];
874 $filename = $parts[$i + 1];
875 $originalFilename = $filename;
876 $optionalProperties = $parts[$i + 2];
877 $tsContentsTillNextInclude = $parts[$i + 3];
878
879 // Check condition
880 $matches = preg_split('#(?i)condition\\s*=\\s*"((?:\\\\\\\\|\\\\"|[^\\"])*)"(\\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
881 // If there was a condition
882 if (count($matches) > 1) {
883 // Unescape the condition
884 $condition = trim(stripslashes($matches[1]));
885 // If necessary put condition in square brackets
886 if ($condition[0] !== '[') {
887 $condition = '[' . $condition . ']';
888 }
889
890 /** @var AbstractConditionMatcher $conditionMatcher */
891 $conditionMatcher = null;
892 if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE) {
893 $conditionMatcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class);
894 } else {
895 $conditionMatcher = GeneralUtility::makeInstance(BackendConditionMatcher::class);
896 }
897
898 // If it didn't match then proceed to the next include, but prepend next normal (not file) part to output string
899 if (!$conditionMatcher->match($condition)) {
900 $newString .= $tsContentsTillNextInclude . LF;
901 continue;
902 }
903 }
904
905 // Resolve a possible relative paths if a parent file is given
906 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
907 $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
908 }
909
910 // There must be a line-break char after - not sure why this check is necessary, kept it for being 100% backwards compatible
911 // An empty string is also ok (means that the next line is also a valid include_typoscript tag)
912 if (!preg_match('/(^\\s*\\r?\\n|^$)/', $tsContentsTillNextInclude)) {
913 $newString .= self::typoscriptIncludeError('Invalid characters after <INCLUDE_TYPOSCRIPT: source="' . $includeType . ':' . $filename . '">-tag (rest of line must be empty).');
914 } elseif (strpos('..', $filename) !== false) {
915 $newString .= self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
916 } else {
917 switch (strtolower($includeType)) {
918 case 'file':
919 self::includeFile($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
920 break;
921 case 'dir':
922 self::includeDirectory($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
923 break;
924 default:
925 $newString .= self::typoscriptIncludeError('No valid option for INCLUDE_TYPOSCRIPT source property (valid options are FILE or DIR)');
926 }
927 }
928 // Prepend next normal (not file) part to output string
929 $newString .= $tsContentsTillNextInclude . LF;
930
931 // load default TypoScript for content rendering templates like
932 // fluid_styled_content if those have been included through f.e.
933 // <INCLUDE_TYPOSCRIPT: source="FILE:EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript">
934 if (strpos(strtolower($filename), 'ext:') === 0) {
935 $filePointerPathParts = explode('/', substr($filename, 4));
936
937 // remove file part, determine whether to load setup or constants
938 list($includeType, ) = explode('.', array_pop($filePointerPathParts));
939
940 if (in_array($includeType, ['setup', 'constants'])) {
941 // adapt extension key to required format (no underscores)
942 $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
943
944 // load default TypoScript
945 $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
946 if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
947 $newString .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
948 }
949 }
950 }
951 }
952 // Add a line break before and after the included code in order to make sure that the parser always has a LF.
953 $string = LF . trim($newString) . LF;
954 }
955 // When all included files should get returned, simply return an compound array containing
956 // the TypoScript with all "includes" processed and the files which got included
957 if ($returnFiles) {
958 return [
959 'typoscript' => $string,
960 'files' => $includedFiles
961 ];
962 }
963 return $string;
964 }
965
966 /**
967 * Splits the unparsed TypoScript content into @import statements
968 *
969 * @param string $typoScript unparsed TypoScript
970 * @param int $cycleCounter counter to stop recursion
971 * @param bool $returnFiles whether to populate the included Files or not
972 * @param array $includedFiles - by reference - if any included files are added, they are added here
973 * @param string $parentFilenameOrPath the current imported file to resolve relative paths - handled by reference
974 * @return string the unparsed TypoScript with included external files
975 */
976 protected static function addImportsFromExternalFiles($typoScript, $cycleCounter, $returnFiles, &$includedFiles, &$parentFilenameOrPath)
977 {
978 // Check for new syntax "@import 'EXT:bennilove/Configuration/TypoScript/*'"
979 if (strpos($typoScript, '@import \'') !== false || strpos($typoScript, '@import "') !== false) {
980 $splitRegEx = '/\r?\n\s*@import\s[\'"]([^\'"]*)[\'"][\ \t]?/';
981 $parts = preg_split($splitRegEx, LF . $typoScript . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
982 // First text part goes through
983 $newString = $parts[0] . LF;
984 $partCount = count($parts);
985 for ($i = 1; $i + 2 <= $partCount; $i += 2) {
986 $filename = $parts[$i];
987 $tsContentsTillNextInclude = $parts[$i + 1];
988 // Resolve a possible relative paths if a parent file is given
989 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
990 $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
991 }
992 $newString .= self::importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, $includedFiles);
993 // Prepend next normal (not file) part to output string
994 $newString .= $tsContentsTillNextInclude;
995 }
996 // Add a line break before and after the included code in order to make sure that the parser always has a LF.
997 $typoScript = LF . trim($newString) . LF;
998 }
999 return $typoScript;
1000 }
1001
1002 /**
1003 * Include file $filename. Contents of the file will be returned, filename is added to &$includedFiles.
1004 * Further include/import statements in the contents are processed recursively.
1005 *
1006 * @param string $filename Full absolute path+filename to the typoscript file to be included
1007 * @param int $cycleCounter Counter for detecting endless loops
1008 * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1009 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1010 * @return string the unparsed TypoScript content from external files
1011 */
1012 protected static function importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, array &$includedFiles)
1013 {
1014 if (strpos('..', $filename) !== false) {
1015 return self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
1016 }
1017
1018 $content = '';
1019 $absoluteFileName = GeneralUtility::getFileAbsFileName($filename);
1020 if ((string)$absoluteFileName === '') {
1021 return self::typoscriptIncludeError('Illegal filepath "' . $filename . '".');
1022 }
1023
1024 $finder = new Finder();
1025 $finder
1026 // no recursive mode on purpose
1027 ->depth(0)
1028 // no directories should be fetched
1029 ->files()
1030 ->sortByName();
1031
1032 // Search all files in the folder
1033 if (is_dir($absoluteFileName)) {
1034 $finder->in($absoluteFileName);
1035 // Used for the TypoScript comments
1036 $readableFilePrefix = $filename;
1037 } else {
1038 try {
1039 // Apparently this is not a folder, so the restriction
1040 // is the folder so we restrict into this folder
1041 $finder->in(PathUtility::dirname($absoluteFileName));
1042 if (!is_file($absoluteFileName)
1043 && strpos(PathUtility::basename($absoluteFileName), '*') === false
1044 && substr(PathUtility::basename($absoluteFileName), -11) !== '.typoscript') {
1045 $absoluteFileName .= '*.typoscript';
1046 }
1047 $finder->name(PathUtility::basename($absoluteFileName));
1048 $readableFilePrefix = PathUtility::dirname($filename);
1049 } catch (\InvalidArgumentException $e) {
1050 return self::typoscriptIncludeError($e->getMessage());
1051 }
1052 }
1053
1054 foreach ($finder as $fileObject) {
1055 // Clean filename output for comments
1056 $readableFileName = rtrim($readableFilePrefix, '/') . '/' . $fileObject->getFilename();
1057 $content .= LF . '### @import \'' . $readableFileName . '\' begin ###' . LF;
1058 // Check for allowed files
1059 if (!GeneralUtility::verifyFilenameAgainstDenyPattern($fileObject->getFilename())) {
1060 $content .= self::typoscriptIncludeError('File "' . $readableFileName . '" was not included since it is not allowed due to fileDenyPattern.');
1061 } else {
1062 $includedFiles[] = $fileObject->getPathname();
1063 // check for includes in included text
1064 $included_text = self::checkIncludeLines($fileObject->getContents(), $cycleCounter++, $returnFiles, $absoluteFileName);
1065 // If the method also has to return all included files, merge currently included
1066 // files with files included by recursively calling itself
1067 if ($returnFiles && is_array($included_text)) {
1068 $includedFiles = array_merge($includedFiles, $included_text['files']);
1069 $included_text = $included_text['typoscript'];
1070 }
1071 $content .= $included_text . LF;
1072 }
1073 $content .= '### @import \'' . $readableFileName . '\' end ###' . LF . LF;
1074
1075 // load default TypoScript for content rendering templates like
1076 // fluid_styled_content if those have been included through e.g.
1077 // @import "fluid_styled_content/Configuration/TypoScript/setup.typoscript"
1078 if (strpos(strtoupper($filename), 'EXT:') === 0) {
1079 $filePointerPathParts = explode('/', substr($filename, 4));
1080 // remove file part, determine whether to load setup or constants
1081 list($includeType) = explode('.', array_pop($filePointerPathParts));
1082
1083 if (in_array($includeType, ['setup', 'constants'], true)) {
1084 // adapt extension key to required format (no underscores)
1085 $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
1086
1087 // load default TypoScript
1088 $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
1089 if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
1090 $content .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
1091 }
1092 }
1093 }
1094 }
1095
1096 if (empty($content)) {
1097 return self::typoscriptIncludeError('No file or folder found for importing TypoScript on "' . $filename . '".');
1098 }
1099 return $content;
1100 }
1101
1102 /**
1103 * Include file $filename. Contents of the file will be prepended to &$newstring, filename to &$includedFiles
1104 * Further include_typoscript tags in the contents are processed recursively
1105 *
1106 * @param string $filename Relative path to the typoscript file to be included
1107 * @param int $cycle_counter Counter for detecting endless loops
1108 * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1109 * @param string &$newString The output string to which the content of the file will be prepended (referenced
1110 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1111 * @param string $optionalProperties
1112 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1113 * @static
1114 * @internal
1115 */
1116 public static function includeFile($filename, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1117 {
1118 // Resolve a possible relative paths if a parent file is given
1119 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
1120 $absfilename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
1121 } else {
1122 $absfilename = $filename;
1123 }
1124 $absfilename = GeneralUtility::getFileAbsFileName($absfilename);
1125
1126 $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> BEGIN:' . LF;
1127 if ((string)$filename !== '') {
1128 // Must exist and must not contain '..' and must be relative
1129 // Check for allowed files
1130 if (!GeneralUtility::verifyFilenameAgainstDenyPattern($absfilename)) {
1131 $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
1132 } else {
1133 $fileExists = false;
1134 if (@file_exists($absfilename)) {
1135 $fileExists = true;
1136 } else {
1137 // BC layer after renaming core TypoScript files from .txt to .typoscript
1138 if (substr($absfilename, -4, 4) === '.txt') {
1139 $absfilename = substr($absfilename, 0, -4) . '.typoscript';
1140 if (@file_exists($absfilename)) {
1141 trigger_error('The TypoScript file ' . $filename . ' was renamed to .typoscript extension.'
1142 . ' Update your "<INCLUDE_TYPOSCRIPT" statements.', E_USER_DEPRECATED);
1143 $fileExists = true;
1144 }
1145 }
1146 }
1147
1148 if ($fileExists) {
1149 $includedFiles[] = $absfilename;
1150 // check for includes in included text
1151 $included_text = self::checkIncludeLines(file_get_contents($absfilename), $cycle_counter + 1, $returnFiles, $absfilename);
1152 // If the method also has to return all included files, merge currently included
1153 // files with files included by recursively calling itself
1154 if ($returnFiles && is_array($included_text)) {
1155 $includedFiles = array_merge($includedFiles, $included_text['files']);
1156 $included_text = $included_text['typoscript'];
1157 }
1158 $newString .= $included_text . LF;
1159 } else {
1160 $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not found.');
1161 }
1162 }
1163 }
1164 $newString .= '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> END:' . LF . LF;
1165 }
1166
1167 /**
1168 * Include all files with matching Typoscript extensions in directory $dirPath. Contents of the files are
1169 * prepended to &$newstring, filename to &$includedFiles.
1170 * Order of the directory items to be processed: files first, then directories, both in alphabetical order.
1171 * Further include_typoscript tags in the contents of the files are processed recursively.
1172 *
1173 * @param string $dirPath Relative path to the directory to be included
1174 * @param int $cycle_counter Counter for detecting endless loops
1175 * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1176 * @param string &$newString The output string to which the content of the file will be prepended (referenced)
1177 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1178 * @param string $optionalProperties
1179 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1180 * @static
1181 */
1182 protected static function includeDirectory($dirPath, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1183 {
1184 // Extract the value of the property extensions="..."
1185 $matches = preg_split('#(?i)extensions\s*=\s*"([^"]*)"(\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
1186 if (count($matches) > 1) {
1187 $includedFileExtensions = $matches[1];
1188 } else {
1189 $includedFileExtensions = '';
1190 }
1191
1192 // Resolve a possible relative paths if a parent file is given
1193 if ($parentFilenameOrPath !== '' && $dirPath[0] === '.') {
1194 $resolvedDirPath = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $dirPath);
1195 } else {
1196 $resolvedDirPath = $dirPath;
1197 }
1198 $absDirPath = GeneralUtility::getFileAbsFileName($resolvedDirPath);
1199 if ($absDirPath) {
1200 $absDirPath = rtrim($absDirPath, '/') . '/';
1201 $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> BEGIN:' . LF;
1202 // Get alphabetically sorted file index in array
1203 $fileIndex = GeneralUtility::getAllFilesAndFoldersInPath([], $absDirPath, $includedFileExtensions);
1204 // Prepend file contents to $newString
1205 $prefixLength = strlen(Environment::getPublicPath() . '/');
1206 foreach ($fileIndex as $absFileRef) {
1207 $relFileRef = substr($absFileRef, $prefixLength);
1208 self::includeFile($relFileRef, $cycle_counter, $returnFiles, $newString, $includedFiles, '', $absDirPath);
1209 }
1210 $newString .= '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> END:' . LF . LF;
1211 } else {
1212 $newString .= self::typoscriptIncludeError('The path "' . $resolvedDirPath . '" is invalid.');
1213 }
1214 }
1215
1216 /**
1217 * Process errors in INCLUDE_TYPOSCRIPT tags
1218 * Errors are logged and printed in the concatenated TypoScript result (as can be seen in Template Analyzer)
1219 *
1220 * @param string $error Text of the error message
1221 * @return string The error message encapsulated in comments
1222 * @static
1223 */
1224 protected static function typoscriptIncludeError($error)
1225 {
1226 self::getLogger()->warning($error);
1227 return "\n###\n### ERROR: " . $error . "\n###\n\n";
1228 }
1229
1230 /**
1231 * Parses the string in each value of the input array for include-commands
1232 *
1233 * @param array $array Array with TypoScript in each value
1234 * @return array Same array but where the values has been parsed for include-commands
1235 */
1236 public static function checkIncludeLines_array(array $array)
1237 {
1238 foreach ($array as $k => $v) {
1239 $array[$k] = self::checkIncludeLines($array[$k]);
1240 }
1241 return $array;
1242 }
1243
1244 /**
1245 * Search for commented INCLUDE_TYPOSCRIPT statements
1246 * and save the content between the BEGIN and the END line to the specified file
1247 *
1248 * @param string $string Template content
1249 * @param int $cycle_counter Counter for detecting endless loops
1250 * @param array $extractedFileNames
1251 * @param string $parentFilenameOrPath
1252 *
1253 * @throws \RuntimeException
1254 * @throws \UnexpectedValueException
1255 * @return string Template content with uncommented include statements
1256 * @internal
1257 */
1258 public static function extractIncludes($string, $cycle_counter = 1, array $extractedFileNames = [], $parentFilenameOrPath = '')
1259 {
1260 if ($cycle_counter > 10) {
1261 self::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
1262 return '
1263 ###
1264 ### ERROR: Recursion!
1265 ###
1266 ';
1267 }
1268 $expectedEndTag = '';
1269 $fileContent = [];
1270 $restContent = [];
1271 $fileName = null;
1272 $inIncludePart = false;
1273 $lines = preg_split("/\r\n|\n|\r/", $string);
1274 $skipNextLineIfEmpty = false;
1275 $openingCommentedIncludeStatement = null;
1276 $optionalProperties = '';
1277 foreach ($lines as $line) {
1278 // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1279 // an additional empty line, remove this again
1280 if ($skipNextLineIfEmpty) {
1281 if (trim($line) === '') {
1282 continue;
1283 }
1284 $skipNextLineIfEmpty = false;
1285 }
1286
1287 // Outside commented include statements
1288 if (!$inIncludePart) {
1289 // Search for beginning commented include statements
1290 if (preg_match('/###\\s*<INCLUDE_TYPOSCRIPT:\\s*source\\s*=\\s*"\\s*((?i)file|dir)\\s*:\\s*([^"]*)"(.*)>\\s*BEGIN/i', $line, $matches)) {
1291 // Found a commented include statement
1292
1293 // Save this line in case there is no ending tag
1294 $openingCommentedIncludeStatement = trim($line);
1295 $openingCommentedIncludeStatement = preg_replace('/\\s*### Warning: .*###\\s*/', '', $openingCommentedIncludeStatement);
1296
1297 // type of match: FILE or DIR
1298 $inIncludePart = strtoupper($matches[1]);
1299 $fileName = $matches[2];
1300 $optionalProperties = $matches[3];
1301
1302 $expectedEndTag = '### <INCLUDE_TYPOSCRIPT: source="' . $inIncludePart . ':' . $fileName . '"' . $optionalProperties . '> END';
1303 // Strip all whitespace characters to make comparison safer
1304 $expectedEndTag = strtolower(preg_replace('/\s/', '', $expectedEndTag));
1305 } else {
1306 // If this is not a beginning commented include statement this line goes into the rest content
1307 $restContent[] = $line;
1308 }
1309 } else {
1310 // Inside commented include statements
1311 // Search for the matching ending commented include statement
1312 $strippedLine = preg_replace('/\s/', '', $line);
1313 if (stripos($strippedLine, $expectedEndTag) !== false) {
1314 // Found the matching ending include statement
1315 $fileContentString = implode(PHP_EOL, $fileContent);
1316
1317 // Write the content to the file
1318
1319 // Resolve a possible relative paths if a parent file is given
1320 if ($parentFilenameOrPath !== '' && $fileName[0] === '.') {
1321 $realFileName = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $fileName);
1322 } else {
1323 $realFileName = $fileName;
1324 }
1325 $realFileName = GeneralUtility::getFileAbsFileName($realFileName);
1326
1327 if ($inIncludePart === 'FILE') {
1328 // Some file checks
1329 if (!GeneralUtility::verifyFilenameAgainstDenyPattern($realFileName)) {
1330 throw new \UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
1331 }
1332 if (empty($realFileName)) {
1333 throw new \UnexpectedValueException(sprintf('"%s" is not a valid file location.', $fileName), 1294586441);
1334 }
1335 if (!is_writable($realFileName)) {
1336 throw new \RuntimeException(sprintf('"%s" is not writable.', $fileName), 1294586442);
1337 }
1338 if (in_array($realFileName, $extractedFileNames)) {
1339 throw new \RuntimeException(sprintf('Recursive/multiple inclusion of file "%s"', $realFileName), 1294586443);
1340 }
1341 $extractedFileNames[] = $realFileName;
1342
1343 // Recursive call to detected nested commented include statements
1344 $fileContentString = self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1345
1346 // Write the content to the file
1347 if (!GeneralUtility::writeFile($realFileName, $fileContentString)) {
1348 throw new \RuntimeException(sprintf('Could not write file "%s"', $realFileName), 1294586444);
1349 }
1350 // Insert reference to the file in the rest content
1351 $restContent[] = '<INCLUDE_TYPOSCRIPT: source="FILE:' . $fileName . '"' . $optionalProperties . '>';
1352 } else {
1353 // must be DIR
1354
1355 // Some file checks
1356 if (empty($realFileName)) {
1357 throw new \UnexpectedValueException(sprintf('"%s" is not a valid location.', $fileName), 1366493602);
1358 }
1359 if (!is_dir($realFileName)) {
1360 throw new \RuntimeException(sprintf('"%s" is not a directory.', $fileName), 1366493603);
1361 }
1362 if (in_array($realFileName, $extractedFileNames)) {
1363 throw new \RuntimeException(sprintf('Recursive/multiple inclusion of directory "%s"', $realFileName), 1366493604);
1364 }
1365 $extractedFileNames[] = $realFileName;
1366
1367 // Recursive call to detected nested commented include statements
1368 self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1369
1370 // just drop content between tags since it should usually just contain individual files from that dir
1371
1372 // Insert reference to the dir in the rest content
1373 $restContent[] = '<INCLUDE_TYPOSCRIPT: source="DIR:' . $fileName . '"' . $optionalProperties . '>';
1374 }
1375
1376 // Reset variables (preparing for the next commented include statement)
1377 $fileContent = [];
1378 $fileName = null;
1379 $inIncludePart = false;
1380 $openingCommentedIncludeStatement = null;
1381 // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1382 // an additional empty line, remove this again
1383 $skipNextLineIfEmpty = true;
1384 } else {
1385 // If this is not an ending commented include statement this line goes into the file content
1386 $fileContent[] = $line;
1387 }
1388 }
1389 }
1390 // If we're still inside commented include statements copy the lines back to the rest content
1391 if ($inIncludePart) {
1392 $restContent[] = $openingCommentedIncludeStatement . ' ### Warning: Corresponding end line missing! ###';
1393 $restContent = array_merge($restContent, $fileContent);
1394 }
1395 $restContentString = implode(PHP_EOL, $restContent);
1396 return $restContentString;
1397 }
1398
1399 /**
1400 * Processes the string in each value of the input array with extractIncludes
1401 *
1402 * @param array $array Array with TypoScript in each value
1403 * @return array Same array but where the values has been processed with extractIncludes
1404 */
1405 public static function extractIncludes_array(array $array)
1406 {
1407 foreach ($array as $k => $v) {
1408 $array[$k] = self::extractIncludes($array[$k]);
1409 }
1410 return $array;
1411 }
1412
1413 /**********************************
1414 *
1415 * Syntax highlighting
1416 *
1417 *********************************/
1418 /**
1419 * Syntax highlight a TypoScript text
1420 * Will parse the content. Remember, the internal setup array may contain invalid parsed content since conditions are ignored!
1421 *
1422 * @param string $string The TypoScript text
1423 * @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.
1424 * @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.
1425 * @return string HTML code for the syntax highlighted string
1426 */
1427 public function doSyntaxHighlight($string, $lineNum = '', $highlightBlockMode = false)
1428 {
1429 $this->syntaxHighLight = true;
1430 $this->highLightData = [];
1431 $this->errors = [];
1432 // 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.
1433 $string = str_replace(CR, '', $string);
1434 $this->parse($string);
1435 return $this->syntaxHighlight_print($lineNum, $highlightBlockMode);
1436 }
1437
1438 /**
1439 * Registers a part of a TypoScript line for syntax highlighting.
1440 *
1441 * @param string $code Key from the internal array $this->highLightStyles
1442 * @param int $pointer Pointer to the line in $this->raw which this is about
1443 * @param int $strlen The number of chars LEFT on this line before the end is reached.
1444 * @see parse()
1445 */
1446 protected function regHighLight($code, $pointer, $strlen = -1)
1447 {
1448 if ($strlen === -1) {
1449 $this->highLightData[$pointer] = [[$code, 0]];
1450 } else {
1451 $this->highLightData[$pointer][] = [$code, $strlen];
1452 }
1453 $this->highLightData_bracelevel[$pointer] = $this->inBrace;
1454 }
1455
1456 /**
1457 * Formatting the TypoScript code in $this->raw based on the data collected by $this->regHighLight in $this->highLightData
1458 *
1459 * @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.
1460 * @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.
1461 * @return string HTML content
1462 * @see doSyntaxHighlight()
1463 */
1464 protected function syntaxHighlight_print($lineNumDat, $highlightBlockMode)
1465 {
1466 // Registers all error messages in relation to their linenumber
1467 $errA = [];
1468 foreach ($this->errors as $err) {
1469 $errA[$err[2]][] = $err[0];
1470 }
1471 // Generates the syntax highlighted output:
1472 $lines = [];
1473 foreach ($this->raw as $rawP => $value) {
1474 $start = 0;
1475 $strlen = strlen($value);
1476 $lineC = '';
1477 if (is_array($this->highLightData[$rawP])) {
1478 foreach ($this->highLightData[$rawP] as $set) {
1479 $len = $strlen - $start - $set[1];
1480 if ($len > 0) {
1481 $part = substr($value, $start, $len);
1482 $start += $len;
1483 $st = $this->highLightStyles[isset($this->highLightStyles[$set[0]]) ? $set[0] : 'default'];
1484 if (!$highlightBlockMode || $set[0] !== 'prespace') {
1485 $lineC .= $st[0] . htmlspecialchars($part) . $st[1];
1486 }
1487 } elseif ($len < 0) {
1488 debug([$len, $value, $rawP]);
1489 }
1490 }
1491 } else {
1492 debug([$value]);
1493 }
1494 if (strlen($value) > $start) {
1495 $lineC .= $this->highLightStyles['ignored'][0] . htmlspecialchars(substr($value, $start)) . $this->highLightStyles['ignored'][1];
1496 }
1497 if ($errA[$rawP]) {
1498 $lineC .= $this->highLightStyles['error'][0] . '<strong> - ERROR:</strong> ' . htmlspecialchars(implode(';', $errA[$rawP])) . $this->highLightStyles['error'][1];
1499 }
1500 if ($highlightBlockMode && $this->highLightData_bracelevel[$rawP]) {
1501 $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 : '&nbsp;') . '</span>';
1502 }
1503 if (is_array($lineNumDat)) {
1504 $lineNum = $rawP + $lineNumDat[0];
1505 if ($this->parentObject instanceof ExtendedTemplateService) {
1506 $lineNum = $this->parentObject->ext_lnBreakPointWrap($lineNum, $lineNum);
1507 }
1508 $lineC = $this->highLightStyles['linenum'][0] . str_pad($lineNum, 4, ' ', STR_PAD_LEFT) . ':' . $this->highLightStyles['linenum'][1] . ' ' . $lineC;
1509 }
1510 $lines[] = $lineC;
1511 }
1512 return '<pre class="ts-hl">' . implode(LF, $lines) . '</pre>';
1513 }
1514
1515 /**
1516 * @return TimeTracker
1517 */
1518 protected function getTimeTracker()
1519 {
1520 return GeneralUtility::makeInstance(TimeTracker::class);
1521 }
1522
1523 /**
1524 * Modifies a HTML Hex color by adding/subtracting $R,$G and $B integers
1525 *
1526 * @param string $color A hexadecimal color code, #xxxxxx
1527 * @param int $R Offset value 0-255
1528 * @param int $G Offset value 0-255
1529 * @param int $B Offset value 0-255
1530 * @return string A hexadecimal color code, #xxxxxx, modified according to input vars
1531 * @see modifyHTMLColorAll()
1532 */
1533 protected function modifyHTMLColor($color, $R, $G, $B)
1534 {
1535 // This takes a hex-color (# included!) and adds $R, $G and $B to the HTML-color (format: #xxxxxx) and returns the new color
1536 $nR = MathUtility::forceIntegerInRange(hexdec(substr($color, 1, 2)) + $R, 0, 255);
1537 $nG = MathUtility::forceIntegerInRange(hexdec(substr($color, 3, 2)) + $G, 0, 255);
1538 $nB = MathUtility::forceIntegerInRange(hexdec(substr($color, 5, 2)) + $B, 0, 255);
1539 return '#' . substr('0' . dechex($nR), -2) . substr('0' . dechex($nG), -2) . substr('0' . dechex($nB), -2);
1540 }
1541
1542 /**
1543 * Modifies a HTML Hex color by adding/subtracting $all integer from all R/G/B channels
1544 *
1545 * @param string $color A hexadecimal color code, #xxxxxx
1546 * @param int $all Offset value 0-255 for all three channels.
1547 * @return string A hexadecimal color code, #xxxxxx, modified according to input vars
1548 * @see modifyHTMLColor()
1549 */
1550 protected function modifyHTMLColorAll($color, $all)
1551 {
1552 return $this->modifyHTMLColor($color, $all, $all, $all);
1553 }
1554
1555 /**
1556 * Get a logger instance
1557 *
1558 * This class uses logging mostly in static functions, hence we need a static getter for the logger.
1559 * Injection of a logger instance via GeneralUtility::makeInstance is not possible.
1560 *
1561 * @return LoggerInterface
1562 */
1563 protected static function getLogger()
1564 {
1565 return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
1566 }
1567 }