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