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