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