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