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