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