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