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