2 namespace TYPO3\CMS\Core\TypoScript\Parser
;
5 * This file is part of the TYPO3 CMS project.
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.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
16 use TYPO3\CMS\Core\Utility\GeneralUtility
;
17 use \TYPO3\CMS\Core\Utility\PathUtility
;
20 * The TypoScript parser
22 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
24 class TypoScriptParser
{
26 // If set, then key names cannot contain characters other than [:alnum:]_\.-
28 * @todo Define visibility
33 // TypoScript hierarchy being build during parsing.
35 * @todo Define visibility
37 public $setup = array();
39 // Raw data, the input string exploded by LF
41 * @todo Define visibility
45 // Pointer to entry in raw data array
47 * @todo Define visibility
51 // Holding the value of the last comment
53 * @todo Define visibility
55 public $lastComment = '';
57 // Internally set, used as internal flag to create a multi-line comment (one of those like /*... */)
59 * @todo Define visibility
61 public $commentSet = 0;
63 // Internally set, when multiline value is accumulated
65 * @todo Define visibility
67 public $multiLineEnabled = 0;
69 // Internally set, when multiline value is accumulated
71 * @todo Define visibility
73 public $multiLineObject = '';
75 // Internally set, when multiline value is accumulated
77 * @todo Define visibility
79 public $multiLineValue = array();
81 // Internally set, when in brace. Counter.
83 * @todo Define visibility
87 // For each condition this flag is set, if the condition is TRUE, else it's cleared. Then it's used by the [ELSE] condition to determine if the next part should be parsed.
89 * @todo Define visibility
91 public $lastConditionTrue = 1;
93 // Tracking all conditions found
95 * @todo Define visibility
97 public $sections = array();
99 // Tracking all matching conditions found
101 * @todo Define visibility
103 public $sectionsMatch = array();
105 // If set, then syntax highlight mode is on; Call the function syntaxHighlight() to use this function
107 * @todo Define visibility
109 public $syntaxHighLight = 0;
111 // Syntax highlight data is accumulated in this array. Used by syntaxHighlight_print() to construct the output.
113 * @todo Define visibility
115 public $highLightData = array();
117 // Syntax highlight data keeping track of the curly brace level for each line
119 * @todo Define visibility
121 public $highLightData_bracelevel = array();
123 // Debugging, analysis:
124 // DO NOT register the comments. This is default for the ordinary sitetemplate!
126 * @todo Define visibility
128 public $regComments = 0;
130 // DO NOT register the linenumbers. This is default for the ordinary sitetemplate!
132 * @todo Define visibility
134 public $regLinenumbers = 0;
136 // Error accumulation array.
138 * @todo Define visibility
140 public $errors = array();
142 // Used for the error messages line number reporting. Set externally.
144 * @todo Define visibility
146 public $lineNumberOffset = 0;
148 // Line for break point.
150 * @todo Define visibility
152 public $breakPointLN = 0;
155 * @todo Define visibility
157 public $highLightStyles = array(
158 'prespace' => array('<span class="ts-prespace">', '</span>'),
159 // Space before any content on a line
160 'objstr_postspace' => array('<span class="ts-objstr_postspace">', '</span>'),
161 // Space after the object string on a line
162 'operator_postspace' => array('<span class="ts-operator_postspace">', '</span>'),
163 // Space after the operator on a line
164 'operator' => array('<span class="ts-operator">', '</span>'),
166 'value' => array('<span class="ts-value">', '</span>'),
167 // The value of a line
168 'objstr' => array('<span class="ts-objstr">', '</span>'),
169 // The object string of a line
170 'value_copy' => array('<span class="ts-value_copy">', '</span>'),
171 // The value when the copy syntax (<) is used; that means the object reference
172 'value_unset' => array('<span class="ts-value_unset">', '</span>'),
173 // The value when an object is unset. Should not exist.
174 'ignored' => array('<span class="ts-ignored">', '</span>'),
175 // The "rest" of a line which will be ignored.
176 'default' => array('<span class="ts-default">', '</span>'),
177 // The default style if none other is applied.
178 'comment' => array('<span class="ts-comment">', '</span>'),
180 'condition' => array('<span class="ts-condition">', '</span>'),
182 'error' => array('<span class="ts-error">', '</span>'),
184 'linenum' => array('<span class="ts-linenum">', '</span>')
187 // Additional attributes for the <span> tags for a blockmode line
189 * @todo Define visibility
191 public $highLightBlockStyles = '';
193 // The hex-HTML color for the blockmode
195 * @todo Define visibility
197 public $highLightBlockStyles_basecolor = '#cccccc';
199 //Instance of parentObject, used by \TYPO3\CMS\Core\TypoScript\ExtendedTemplateService
200 public $parentObject;
203 * Start parsing the input TypoScript text piece. The result is stored in $this->setup
205 * @param string $string The TypoScript text
206 * @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])
209 * @todo Define visibility
211 public function parse($string, $matchObj = '') {
212 $this->raw
= explode(LF
, $string);
216 if ($this->breakPointLN
&& $pre === '[_BREAK]') {
217 $this->error('Breakpoint at ' . ($this->lineNumberOffset +
$this->rawP
- 2) . ': Line content was "' . $this->raw
[($this->rawP
- 2)] . '"', 1);
220 $preUppercase = strtoupper($pre);
221 if ($pre[0] === '[' &&
222 ($preUppercase === '[GLOBAL]' ||
223 $preUppercase === '[END]' ||
224 !$this->lastConditionTrue
&& $preUppercase === '[ELSE]')
226 $pre = trim($this->parseSub($this->setup
));
227 $this->lastConditionTrue
= 1;
229 // We're in a specific section. Therefore we log this section
230 $specificSection = $preUppercase !== '[ELSE]';
231 if ($specificSection) {
232 $this->sections
[md5($pre)] = $pre;
234 if (is_object($matchObj) && $matchObj->match($pre) ||
$this->syntaxHighLight
) {
235 if ($specificSection) {
236 $this->sectionsMatch
[md5($pre)] = $pre;
238 $pre = trim($this->parseSub($this->setup
));
239 $this->lastConditionTrue
= 1;
241 $pre = $this->nextDivider();
242 $this->lastConditionTrue
= 0;
246 if ($this->inBrace
) {
247 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': The script is short of ' . $this->inBrace
. ' end brace(s)', 1);
249 if ($this->multiLineEnabled
) {
250 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': A multiline value section is not ended with a parenthesis!', 1);
252 $this->lineNumberOffset +
= count($this->raw
) +
1;
256 * 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.
258 * @return string The condition value
260 * @todo Define visibility
262 public function nextDivider() {
263 while (isset($this->raw
[$this->rawP
])) {
264 $line = trim($this->raw
[$this->rawP
]);
266 if ($line && $line[0] === '[') {
273 * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
275 * @param array $setup Reference to the setup array in which to accumulate the values.
276 * @return string|NULL Returns the string of the condition found, the exit signal or possible nothing (if it completed parsing with no interruptions)
277 * @todo Define visibility
279 public function parseSub(array &$setup) {
280 while (isset($this->raw
[$this->rawP
])) {
281 $line = ltrim($this->raw
[$this->rawP
]);
282 $lineP = $this->rawP
;
284 if ($this->syntaxHighLight
) {
285 $this->regHighLight('prespace', $lineP, strlen($line));
288 // By adding 1 we get that line processed
289 if ($this->breakPointLN
&& $this->lineNumberOffset +
$this->rawP
- 1 === $this->breakPointLN +
1) {
293 if (!$this->multiLineEnabled
&& strpos($line, '/*') === 0) {
294 $this->commentSet
= 1;
296 // If $this->multiLineEnabled we will go and get the line values here because we know, the first if() will be TRUE.
297 if (!$this->commentSet
&& ($line ||
$this->multiLineEnabled
)) {
298 // If multiline is enabled. Escape by ')'
299 if ($this->multiLineEnabled
) {
301 if ($line[0] === ')') {
302 if ($this->syntaxHighLight
) {
303 $this->regHighLight('operator', $lineP, strlen($line) - 1);
306 $this->multiLineEnabled
= 0;
307 $theValue = implode($this->multiLineValue
, LF
);
308 if (strpos($this->multiLineObject
, '.') !== FALSE) {
309 // Set the value deeper.
310 $this->setVal($this->multiLineObject
, $setup, array($theValue));
312 // Set value regularly
313 $setup[$this->multiLineObject
] = $theValue;
314 if ($this->lastComment
&& $this->regComments
) {
315 $setup[$this->multiLineObject
. '..'] .= $this->lastComment
;
317 if ($this->regLinenumbers
) {
318 $setup[$this->multiLineObject
. '.ln..'][] = $this->lineNumberOffset +
$this->rawP
- 1;
322 if ($this->syntaxHighLight
) {
323 $this->regHighLight('value', $lineP);
325 $this->multiLineValue
[] = $this->raw
[$this->rawP
- 1];
327 } elseif ($this->inBrace
=== 0 && $line[0] === '[') {
328 // Beginning of condition (only on level zero compared to brace-levels
329 if ($this->syntaxHighLight
) {
330 $this->regHighLight('condition', $lineP);
334 // Return if GLOBAL condition is set - no matter what.
335 if ($line[0] === '[' && stripos($line, '[GLOBAL]') !== FALSE) {
336 if ($this->syntaxHighLight
) {
337 $this->regHighLight('condition', $lineP);
339 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': On return to [GLOBAL] scope, the script was short of ' . $this->inBrace
. ' end brace(s)', 1);
342 } elseif ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
343 // If not brace-end or comment
344 // Find object name string until we meet an operator
345 $varL = strcspn($line, TAB
. ' {=<>(');
346 // check for special ":=" operator
347 if ($varL > 0 && substr($line, $varL-1, 2) === ':=') {
350 // also remove tabs after the object string name
351 $objStrName = substr($line, 0, $varL);
352 if ($this->syntaxHighLight
) {
353 $this->regHighLight('objstr', $lineP, strlen(substr($line, $varL)));
355 if ($objStrName !== '') {
357 if ($this->strict
&& preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
358 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."');
360 $line = ltrim(substr($line, $varL));
361 if ($this->syntaxHighLight
) {
362 $this->regHighLight('objstr_postspace', $lineP, strlen($line));
364 $this->regHighLight('operator', $lineP, strlen($line) - 1);
365 $this->regHighLight('operator_postspace', $lineP, strlen(ltrim(substr($line, 1))));
369 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
371 // Checking for special TSparser properties (to change TS values at parsetime)
373 if ($line[0] === ':' && preg_match('/^:=\\s*([^\\(]+)\\s*\\((.*)\\).*/', $line, $match)) {
375 $tsFuncArg = $match[2];
376 list($currentValue) = $this->getVal($objStrName, $setup);
377 $tsFuncArg = str_replace(array('\\\\', '\\n', '\\t'), array('\\', LF
, TAB
), $tsFuncArg);
378 $newValue = $this->executeValueModifier($tsFunc, $tsFuncArg, $currentValue);
379 if (isset($newValue)) {
380 $line = '= ' . $newValue;
385 if ($this->syntaxHighLight
) {
386 $this->regHighLight('value', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
388 if (strpos($objStrName, '.') !== FALSE) {
390 $value[0] = trim(substr($line, 1));
391 $this->setVal($objStrName, $setup, $value);
393 $setup[$objStrName] = trim(substr($line, 1));
394 if ($this->lastComment
&& $this->regComments
) {
396 $setup[$objStrName . '..'] .= $this->lastComment
;
398 if ($this->regLinenumbers
) {
399 $setup[$objStrName . '.ln..'][] = $this->lineNumberOffset +
$this->rawP
- 1;
405 if (strpos($objStrName, '.') !== FALSE) {
406 $exitSig = $this->rollParseSub($objStrName, $setup);
411 if (!isset($setup[($objStrName . '.')])) {
412 $setup[$objStrName . '.'] = array();
414 $exitSig = $this->parseSub($setup[$objStrName . '.']);
421 $this->multiLineObject
= $objStrName;
422 $this->multiLineEnabled
= 1;
423 $this->multiLineValue
= array();
426 if ($this->syntaxHighLight
) {
427 $this->regHighLight('value_copy', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
429 $theVal = trim(substr($line, 1));
430 if ($theVal[0] === '.') {
431 $res = $this->getVal(substr($theVal, 1), $setup);
433 $res = $this->getVal($theVal, $this->setup
);
435 $this->setVal($objStrName, $setup, unserialize(serialize($res)), 1);
436 // 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.
439 if ($this->syntaxHighLight
) {
440 $this->regHighLight('value_unset', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
442 $this->setVal($objStrName, $setup, 'UNSET');
445 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
449 $this->lastComment
= '';
451 } elseif ($line[0] === '}') {
453 $this->lastComment
= '';
454 if ($this->syntaxHighLight
) {
455 $this->regHighLight('operator', $lineP, strlen($line) - 1);
457 if ($this->inBrace
< 0) {
458 $this->error('Line ' . ($this->lineNumberOffset +
$this->rawP
- 1) . ': An end brace is in excess.', 1);
464 if ($this->syntaxHighLight
) {
465 $this->regHighLight('comment', $lineP);
467 // Comment. The comments are concatenated in this temporary string:
468 if ($this->regComments
) {
469 $this->lastComment
.= rtrim($line) . LF
;
475 if ($this->commentSet
) {
476 if ($this->syntaxHighLight
) {
477 $this->regHighLight('comment', $lineP);
479 if (strpos($line, '*/') === 0) {
480 $this->commentSet
= 0;
487 * Executes operator functions, called from TypoScript
488 * example: page.10.value := appendString(!)
490 * @param string $modifierName TypoScript function called
491 * @param string $modifierArgument Function arguments; In case of multiple arguments, the method must split on its own
492 * @param string $currentValue Current TypoScript value
493 * @return string Modification result
495 protected function executeValueModifier($modifierName, $modifierArgument = NULL, $currentValue = NULL) {
497 switch ($modifierName) {
498 case 'prependString':
499 $newValue = $modifierArgument . $currentValue;
502 $newValue = $currentValue . $modifierArgument;
505 $newValue = str_replace($modifierArgument, '', $currentValue);
507 case 'replaceString':
508 list($fromStr, $toStr) = explode('|', $modifierArgument, 2);
509 $newValue = str_replace($fromStr, $toStr, $currentValue);
512 $newValue = ((string)$currentValue !== '' ?
$currentValue . ',' : '') . trim($modifierArgument);
514 case 'removeFromList':
515 $existingElements = GeneralUtility
::trimExplode(',', $currentValue);
516 $removeElements = GeneralUtility
::trimExplode(',', $modifierArgument);
517 if (count($removeElements)) {
518 $newValue = implode(',', array_diff($existingElements, $removeElements));
522 $elements = GeneralUtility
::trimExplode(',', $currentValue);
523 $newValue = implode(',', array_unique($elements));
526 $elements = GeneralUtility
::trimExplode(',', $currentValue);
527 $newValue = implode(',', array_reverse($elements));
530 $elements = GeneralUtility
::trimExplode(',', $currentValue);
531 $arguments = GeneralUtility
::trimExplode(',', $modifierArgument);
532 $arguments = array_map('strtolower', $arguments);
533 $sort_flags = SORT_REGULAR
;
534 if (in_array('numeric', $arguments)) {
535 $sort_flags = SORT_NUMERIC
;
537 sort($elements, $sort_flags);
538 if (in_array('descending', $arguments)) {
539 $elements = array_reverse($elements);
541 $newValue = implode(',', $elements);
544 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName])) {
545 $hookMethod = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName];
546 $params = array('currentValue' => $currentValue, 'functionArgument' => $modifierArgument);
548 $newValue = GeneralUtility
::callUserFunction($hookMethod, $params, $fakeThis);
550 GeneralUtility
::sysLog(
551 'Missing function definition for ' . $modifierName . ' on TypoScript',
553 GeneralUtility
::SYSLOG_SEVERITY_WARNING
561 * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
562 * thus having to recursively call itself to get the value
564 * @param string $string The object sub-path, eg "thisprop.another_prot
565 * @param array $setup The local setup array from the function calling this function
566 * @return string Returns the exitSignal
568 * @todo Define visibility
570 public function rollParseSub($string, array &$setup) {
571 if ((string)$string === '') {
575 list($key, $remainingKey) = $this->parseNextKeySegment($string);
577 if (!isset($setup[$key])) {
578 $setup[$key] = array();
580 $exitSig = $remainingKey === ''
581 ?
$this->parseSub($setup[$key])
582 : $this->rollParseSub($remainingKey, $setup[$key]);
583 return $exitSig ?
: '';
587 * Get a value/property pair for an object path in TypoScript, eg. "myobject.myvalue.mysubproperty".
588 * Here: Used by the "copy" operator, <
590 * @param string $string Object path for which to get the value
591 * @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.
592 * @return array An array with keys 0/1 being value/property respectively
593 * @todo Define visibility
595 public function getVal($string, $setup) {
596 if ((string)$string === '') {
600 list($key, $remainingKey) = $this->parseNextKeySegment($string);
601 $subKey = $key . '.';
602 if ($remainingKey === '') {
604 if (isset($setup[$key])) {
605 $retArr[0] = $setup[$key];
607 if (isset($setup[$subKey])) {
608 $retArr[1] = $setup[$subKey];
612 if ($setup[$subKey]) {
613 return $this->getVal($remainingKey, $setup[$subKey]);
620 * Setting a value/property of an object string in the setup array.
622 * @param string $string The object sub-path, eg "thisprop.another_prot
623 * @param array $setup The local setup array from the function calling this function.
624 * @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)
625 * @param bool $wipeOut If set, then both value and property is wiped out when a copy is made of another value.
627 * @todo Define visibility
629 public function setVal($string, array &$setup, $value, $wipeOut = FALSE) {
630 if ((string)$string === '') {
634 list($key, $remainingKey) = $this->parseNextKeySegment($string);
635 $subKey = $key . '.';
636 if ($remainingKey === '') {
637 if ($value === 'UNSET') {
639 unset($setup[$subKey]);
640 if ($this->regLinenumbers
) {
641 $setup[$key . '.ln..'][] = ($this->lineNumberOffset +
$this->rawP
- 1) . '>';
645 if ($wipeOut && $this->strict
) {
647 unset($setup[$subKey]);
648 if ($this->regLinenumbers
) {
649 $setup[$key . '.ln..'][] = ($this->lineNumberOffset +
$this->rawP
- 1) . '<';
653 if (isset($value[0])) {
654 $setup[$key] = $value[0];
656 if (isset($value[1])) {
657 $setup[$subKey] = $value[1];
659 if ($this->lastComment
&& $this->regComments
) {
660 $setup[$key . '..'] .= $this->lastComment
;
662 if ($this->regLinenumbers
&& !$lnRegisDone) {
663 $setup[$key . '.ln..'][] = $this->lineNumberOffset +
$this->rawP
- 1;
667 if (!isset($setup[$subKey])) {
668 $setup[$subKey] = array();
670 $this->setVal($remainingKey, $setup[$subKey], $value);
675 * Determines the first key segment of a TypoScript key by searching for the first
676 * unescaped dot in the given key string.
678 * Since the escape characters are only needed to correctly determine the key
679 * segment any escape characters before the first unescaped dot are
680 * stripped from the key.
682 * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
683 * @return array Array with key segment and remaining part of $key
685 protected function parseNextKeySegment($key) {
686 // if no dot is in the key, nothing to do
687 $dotPosition = strpos($key, '.');
688 if ($dotPosition === FALSE) {
689 return array($key, '');
692 if (strpos($key, '\\') !== FALSE) {
693 // backslashes are in the key, so we do further parsing
695 while ($dotPosition !== FALSE) {
696 if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' ||
$dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
699 // escaped dot found, continue
700 $dotPosition = strpos($key, '.', $dotPosition +
1);
703 if ($dotPosition === FALSE) {
704 // no regular dot found
708 if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
709 $keySegment = substr($key, 0, $dotPosition - 1);
711 $keySegment = substr($key, 0, $dotPosition);
713 $remainingKey = substr($key, $dotPosition +
1);
716 // fix key segment by removing escape sequences
717 $keySegment = str_replace('\\.', '.', $keySegment);
719 // no backslash in the key, we're fine off
720 list($keySegment, $remainingKey) = explode('.', $key, 2);
722 return array($keySegment, $remainingKey);
726 * Stacks errors/messages from the TypoScript parser into an internal array, $this->error
727 * 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.
729 * @param string $err The error message string
730 * @param integer $num The error severity (in the scale of $GLOBALS['TT']->setTSlogMessage: Approx: 2=warning, 1=info, 0=nothing, 3=fatal.)
732 * @todo Define visibility
734 public function error($err, $num = 2) {
735 if (is_object($GLOBALS['TT'])) {
736 $GLOBALS['TT']->setTSlogMessage($err, $num);
738 $this->errors
[] = array($err, $num, $this->rawP
- 1, $this->lineNumberOffset
);
742 * Checks the input string (un-parsed TypoScript) for include-commands ("<INCLUDE_TYPOSCRIPT: ....")
743 * Use: \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines()
745 * @param string $string Unparsed TypoScript
746 * @param integer $cycle_counter Counter for detecting endless loops
747 * @param boolean $returnFiles When set an array containing the resulting typoscript and all included files will get returned
748 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
749 * @return string Complete TypoScript with includes added.
752 static public function checkIncludeLines($string, $cycle_counter = 1, $returnFiles = FALSE, $parentFilenameOrPath = '') {
753 $includedFiles = array();
754 if ($cycle_counter > 100) {
755 GeneralUtility
::sysLog('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags', 'Core', GeneralUtility
::SYSLOG_SEVERITY_WARNING
);
759 'files' => $includedFiles
764 ### ERROR: Recursion!
769 // If no tags found, no need to do slower preg_split
770 if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== FALSE) {
771 $splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
772 $parts = preg_split($splitRegEx, LF
. $string . LF
, -1, PREG_SPLIT_DELIM_CAPTURE
);
773 // First text part goes through
774 $newString = $parts[0] . LF
;
775 $partCount = count($parts);
776 for ($i = 1; $i +
3 < $partCount; $i +
= 4) {
777 // $parts[$i] contains 'FILE' or 'DIR'
778 // $parts[$i+1] contains relative file or directory path to be included
779 // $parts[$i+2] optional properties of the INCLUDE statement
780 // $parts[$i+3] next part of the typoscript string (part in between include-tags)
781 $includeType = $parts[$i];
782 $filename = $parts[$i +
1];
783 $originalFilename = $filename;
784 $optionalProperties = $parts[$i +
2];
785 $tsContentsTillNextInclude = $parts[$i +
3];
787 // Resolve a possible relative paths if a parent file is given
788 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
789 $filename = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
792 // There must be a line-break char after - not sure why this check is necessary, kept it for being 100% backwards compatible
793 // An empty string is also ok (means that the next line is also a valid include_typoscript tag)
794 if (!preg_match('/(^\\s*\\r?\\n|^$)/', $tsContentsTillNextInclude)) {
795 $newString .= self
::typoscriptIncludeError('Invalid characters after <INCLUDE_TYPOSCRIPT: source="' . $includeType . ':' . $filename . '">-tag (rest of line must be empty).');
796 } elseif (strpos('..', $filename) !== FALSE) {
797 $newString .= self
::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
799 switch (strtolower($includeType)) {
801 self
::includeFile($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
804 self
::includeDirectory($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
807 $newString .= self
::typoscriptIncludeError('No valid option for INCLUDE_TYPOSCRIPT source property (valid options are FILE or DIR)');
810 // Prepend next normal (not file) part to output string
811 $newString .= $tsContentsTillNextInclude . LF
;
813 // load default TypoScript for content rendering templates like
814 // css_styled_content if those have been included through f.e.
815 // <INCLUDE_TYPOSCRIPT: source="FILE:EXT:css_styled_content/static/setup.txt">
816 $filePointer = strtolower($filename);
817 if (GeneralUtility
::isFirstPartOfStr($filePointer, 'ext:')) {
818 $filePointerPathParts = explode('/', substr($filePointer, 4));
820 // remove file part, determine whether to load setup or constants
821 list($includeType, ) = explode('.', array_pop($filePointerPathParts));
823 if (in_array($includeType, array('setup', 'constants'))) {
824 // adapt extension key to required format (no underscores)
825 $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
827 // load default TypoScript
828 $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
829 if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], TRUE)) {
830 $newString .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
836 // Not the first/last linebreak char.
837 $string = substr($newString, 1, -1);
839 // When all included files should get returned, simply return an compound array containing
840 // the TypoScript with all "includes" processed and the files which got included
843 'typoscript' => $string,
844 'files' => $includedFiles
851 * Include file $filename. Contents of the file will be prepended to &$newstring, filename to &$includedFiles
852 * Further include_typoscript tags in the contents are processed recursively
854 * @param string $filename Relative path to the typoscript file to be included
855 * @param integer $cycle_counter Counter for detecting endless loops
856 * @param boolean $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
857 * @param string &$newString The output string to which the content of the file will be prepended (referenced
858 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
859 * @param string $optionalProperties
860 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
863 public static function includeFile($filename, $cycle_counter = 1, $returnFiles = FALSE, &$newString = '', array &$includedFiles = array(), $optionalProperties = '', $parentFilenameOrPath = '') {
864 // Resolve a possible relative paths if a parent file is given
865 if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
866 $absfilename = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
868 $absfilename = $filename;
870 $absfilename = GeneralUtility
::getFileAbsFileName($absfilename);
872 $newString .= LF
. '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> BEGIN:' . LF
;
873 if ((string)$filename !== '') {
874 // Must exist and must not contain '..' and must be relative
875 // Check for allowed files
876 if (!GeneralUtility
::verifyFilenameAgainstDenyPattern($absfilename)) {
877 $newString .= self
::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
878 } elseif (!@file_exists
($absfilename)) {
879 $newString .= self
::typoscriptIncludeError('File "' . $filename . '" was not was not found.');
881 $includedFiles[] = $absfilename;
882 // check for includes in included text
883 $included_text = self
::checkIncludeLines(GeneralUtility
::getUrl($absfilename), $cycle_counter +
1, $returnFiles, $absfilename);
884 // If the method also has to return all included files, merge currently included
885 // files with files included by recursively calling itself
886 if ($returnFiles && is_array($included_text)) {
887 $includedFiles = array_merge($includedFiles, $included_text['files']);
888 $included_text = $included_text['typoscript'];
890 $newString .= $included_text . LF
;
893 $newString .= '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> END:' . LF
. LF
;
897 * Include all files with matching Typoscript extensions in directory $dirPath. Contents of the files are
898 * prepended to &$newstring, filename to &$includedFiles.
899 * Order of the directory items to be processed: files first, then directories, both in alphabetical order.
900 * Further include_typoscript tags in the contents of the files are processed recursively.
902 * @param string $dirPath Relative path to the directory to be included
903 * @param integer $cycle_counter Counter for detecting endless loops
904 * @param boolean $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
905 * @param string &$newString The output string to which the content of the file will be prepended (referenced)
906 * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
907 * @param string $optionalProperties
908 * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
911 protected static function includeDirectory($dirPath, $cycle_counter = 1, $returnFiles = FALSE, &$newString = '', array &$includedFiles = array(), $optionalProperties = '', $parentFilenameOrPath = '') {
912 // Extract the value of the property extensions="..."
913 $matches = preg_split('#(?i)extensions\s*=\s*"([^"]*)"(\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE
);
914 if (count($matches) > 1) {
915 $includedFileExtensions = $matches[1];
917 $includedFileExtensions = '';
920 // Resolve a possible relative paths if a parent file is given
921 if ($parentFilenameOrPath !== '' && $dirPath[0] === '.') {
922 $resolvedDirPath = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $dirPath);
924 $resolvedDirPath = $dirPath;
926 $absDirPath = GeneralUtility
::getFileAbsFileName($resolvedDirPath);
928 $absDirPath = rtrim($absDirPath, '/') . '/';
929 $newString .= LF
. '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> BEGIN:' . LF
;
930 // Get alphabetically sorted file index in array
931 $fileIndex = GeneralUtility
::getAllFilesAndFoldersInPath(array(), $absDirPath, $includedFileExtensions);
932 // Prepend file contents to $newString
933 $prefixLength = strlen(PATH_site
);
934 foreach ($fileIndex as $absFileRef) {
935 $relFileRef = substr($absFileRef, $prefixLength);
936 self
::includeFile($relFileRef, $cycle_counter, $returnFiles, $newString, $includedFiles, '', $absDirPath);
938 $newString .= '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> END:' . LF
. LF
;
940 $newString .= self
::typoscriptIncludeError('The path "' . $resolvedDirPath . '" is invalid.');
945 * Process errors in INCLUDE_TYPOSCRIPT tags
946 * Errors are logged in sysLog and printed in the concatenated Typoscript result (as can be seen in Template Analyzer)
948 * @param string $error Text of the error message
949 * @return string The error message encapsulated in comments
952 protected static function typoscriptIncludeError($error) {
953 GeneralUtility
::sysLog($error, 'Core', 2);
954 return "\n###\n### ERROR: " . $error . "\n###\n\n";
958 * Parses the string in each value of the input array for include-commands
960 * @param array $array Array with TypoScript in each value
961 * @return array Same array but where the values has been parsed for include-commands
963 static public function checkIncludeLines_array(array $array) {
964 foreach ($array as $k => $v) {
965 $array[$k] = self
::checkIncludeLines($array[$k]);
971 * Search for commented INCLUDE_TYPOSCRIPT statements
972 * and save the content between the BEGIN and the END line to the specified file
974 * @param string $string Template content
975 * @param integer $cycle_counter Counter for detecting endless loops
976 * @param array $extractedFileNames
977 * @param string $parentFilenameOrPath
979 * @throws \RuntimeException
980 * @throws \UnexpectedValueException
981 * @return string Template content with uncommented include statements
983 static public function extractIncludes($string, $cycle_counter = 1, array $extractedFileNames = array(), $parentFilenameOrPath = '') {
984 if ($cycle_counter > 10) {
985 GeneralUtility
::sysLog('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags', 'Core', GeneralUtility
::SYSLOG_SEVERITY_WARNING
);
988 ### ERROR: Recursion!
992 $expectedEndTag = '';
993 $fileContent = array();
994 $restContent = array();
996 $inIncludePart = FALSE;
997 $lines = preg_split("/\r\n|\n|\r/", $string);
998 $skipNextLineIfEmpty = FALSE;
999 $openingCommentedIncludeStatement = NULL;
1000 $optionalProperties = '';
1001 foreach ($lines as $line) {
1002 // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1003 // an additional empty line, remove this again
1004 if ($skipNextLineIfEmpty) {
1005 if (trim($line) === '') {
1008 $skipNextLineIfEmpty = FALSE;
1011 // Outside commented include statements
1012 if (!$inIncludePart) {
1013 // Search for beginning commented include statements
1014 if (preg_match('/###\\s*<INCLUDE_TYPOSCRIPT:\\s*source\\s*=\\s*"\\s*((?i)file|dir)\\s*:\\s*([^"]*)"(.*)>\\s*BEGIN/i', $line, $matches)) {
1015 // Found a commented include statement
1017 // Save this line in case there is no ending tag
1018 $openingCommentedIncludeStatement = trim($line);
1019 $openingCommentedIncludeStatement = preg_replace('/\\s*### Warning: .*###\\s*/', '', $openingCommentedIncludeStatement);
1021 // type of match: FILE or DIR
1022 $inIncludePart = strtoupper($matches[1]);
1023 $fileName = $matches[2];
1024 $optionalProperties = $matches[3];
1026 $expectedEndTag = '### <INCLUDE_TYPOSCRIPT: source="' . $inIncludePart . ':' . $fileName . '"' . $optionalProperties . '> END';
1027 // Strip all whitespace characters to make comparision safer
1028 $expectedEndTag = strtolower(preg_replace('/\s/', '', $expectedEndTag));
1030 // If this is not a beginning commented include statement this line goes into the rest content
1031 $restContent[] = $line;
1033 //if (is_array($matches)) GeneralUtility::devLog('matches', 'TypoScriptParser', 0, $matches);
1035 // Inside commented include statements
1036 // Search for the matching ending commented include statement
1037 $strippedLine = preg_replace('/\s/', '', $line);
1038 if (stripos($strippedLine, $expectedEndTag) !== FALSE) {
1039 // Found the matching ending include statement
1040 $fileContentString = implode(PHP_EOL
, $fileContent);
1042 // Write the content to the file
1044 // Resolve a possible relative paths if a parent file is given
1045 if ($parentFilenameOrPath !== '' && $fileName[0] === '.') {
1046 $realFileName = PathUtility
::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $fileName);
1048 $realFileName = $fileName;
1050 $realFileName = GeneralUtility
::getFileAbsFileName($realFileName);
1052 if ($inIncludePart === 'FILE') {
1054 if (!GeneralUtility
::verifyFilenameAgainstDenyPattern($realFileName)) {
1055 throw new \
UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
1057 if (empty($realFileName)) {
1058 throw new \
UnexpectedValueException(sprintf('"%s" is not a valid file location.', $fileName), 1294586441);
1060 if (!is_writable($realFileName)) {
1061 throw new \
RuntimeException(sprintf('"%s" is not writable.', $fileName), 1294586442);
1063 if (in_array($realFileName, $extractedFileNames)) {
1064 throw new \
RuntimeException(sprintf('Recursive/multiple inclusion of file "%s"', $realFileName), 1294586443);
1066 $extractedFileNames[] = $realFileName;
1068 // Recursive call to detected nested commented include statements
1069 $fileContentString = self
::extractIncludes($fileContentString, $cycle_counter +
1, $extractedFileNames, $realFileName);
1071 // Write the content to the file
1072 if (!GeneralUtility
::writeFile($realFileName, $fileContentString)) {
1073 throw new \
RuntimeException(sprintf('Could not write file "%s"', $realFileName), 1294586444);
1075 // Insert reference to the file in the rest content
1076 $restContent[] = '<INCLUDE_TYPOSCRIPT: source="FILE:' . $fileName . '"' . $optionalProperties . '>';
1081 if (empty($realFileName)) {
1082 throw new \
UnexpectedValueException(sprintf('"%s" is not a valid location.', $fileName), 1366493602);
1084 if (!is_dir($realFileName)) {
1085 throw new \
RuntimeException(sprintf('"%s" is not a directory.', $fileName), 1366493603);
1087 if (in_array($realFileName, $extractedFileNames)) {
1088 throw new \
RuntimeException(sprintf('Recursive/multiple inclusion of directory "%s"', $realFileName), 1366493604);
1090 $extractedFileNames[] = $realFileName;
1092 // Recursive call to detected nested commented include statements
1093 self
::extractIncludes($fileContentString, $cycle_counter +
1, $extractedFileNames, $realFileName);
1095 // just drop content between tags since it should usually just contain individual files from that dir
1097 // Insert reference to the dir in the rest content
1098 $restContent[] = '<INCLUDE_TYPOSCRIPT: source="DIR:' . $fileName . '"' . $optionalProperties . '>';
1101 // Reset variables (preparing for the next commented include statement)
1102 $fileContent = array();
1104 $inIncludePart = FALSE;
1105 $openingCommentedIncludeStatement = NULL;
1106 // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1107 // an additional empty line, remove this again
1108 $skipNextLineIfEmpty = TRUE;
1110 // If this is not a ending commented include statement this line goes into the file content
1111 $fileContent[] = $line;
1115 // If we're still inside commented include statements copy the lines back to the rest content
1116 if ($inIncludePart) {
1117 $restContent[] = $openingCommentedIncludeStatement . ' ### Warning: Corresponding end line missing! ###';
1118 $restContent = array_merge($restContent, $fileContent);
1120 $restContentString = implode(PHP_EOL
, $restContent);
1121 return $restContentString;
1125 * Processes the string in each value of the input array with extractIncludes
1127 * @param array $array Array with TypoScript in each value
1128 * @return array Same array but where the values has been processed with extractIncludes
1130 static public function extractIncludes_array(array $array) {
1131 foreach ($array as $k => $v) {
1132 $array[$k] = self
::extractIncludes($array[$k]);
1137 /**********************************
1139 * Syntax highlighting
1141 *********************************/
1143 * Syntax highlight a TypoScript text
1144 * Will parse the content. Remember, the internal setup array may contain invalid parsed content since conditions are ignored!
1146 * @param string $string The TypoScript text
1147 * @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.
1148 * @param boolean $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.
1149 * @return string HTML code for the syntax highlighted string
1150 * @todo Define visibility
1152 public function doSyntaxHighlight($string, $lineNum = '', $highlightBlockMode = FALSE) {
1153 $this->syntaxHighLight
= 1;
1154 $this->highLightData
= array();
1155 $this->error
= array();
1156 // 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.
1157 $string = str_replace(CR
, '', $string);
1158 $this->parse($string);
1159 return $this->syntaxHighlight_print($lineNum, $highlightBlockMode);
1163 * Registers a part of a TypoScript line for syntax highlighting.
1165 * @param string $code Key from the internal array $this->highLightStyles
1166 * @param integer $pointer Pointer to the line in $this->raw which this is about
1167 * @param integer $strlen The number of chars LEFT on this line before the end is reached.
1171 * @todo Define visibility
1173 public function regHighLight($code, $pointer, $strlen = -1) {
1174 if ($strlen === -1) {
1175 $this->highLightData
[$pointer] = array(array($code, 0));
1177 $this->highLightData
[$pointer][] = array($code, $strlen);
1179 $this->highLightData_bracelevel
[$pointer] = $this->inBrace
;
1183 * Formatting the TypoScript code in $this->raw based on the data collected by $this->regHighLight in $this->highLightData
1185 * @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.
1186 * @param boolean $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.
1187 * @return string HTML content
1189 * @see doSyntaxHighlight()
1190 * @todo Define visibility
1192 public function syntaxHighlight_print($lineNumDat, $highlightBlockMode) {
1193 // Registers all error messages in relation to their linenumber
1195 foreach ($this->errors
as $err) {
1196 $errA[$err[2]][] = $err[0];
1198 // Generates the syntax highlighted output:
1200 foreach ($this->raw
as $rawP => $value) {
1202 $strlen = strlen($value);
1204 if (is_array($this->highLightData
[$rawP])) {
1205 foreach ($this->highLightData
[$rawP] as $set) {
1206 $len = $strlen - $start - $set[1];
1208 $part = substr($value, $start, $len);
1210 $st = $this->highLightStyles
[isset($this->highLightStyles
[$set[0]]) ?
$set[0] : 'default'];
1211 if (!$highlightBlockMode ||
$set[0] !== 'prespace') {
1212 $lineC .= $st[0] . htmlspecialchars($part) . $st[1];
1214 } elseif ($len < 0) {
1215 debug(array($len, $value, $rawP));
1219 debug(array($value));
1221 if (strlen(substr($value, $start))) {
1222 $lineC .= $this->highLightStyles
['ignored'][0] . htmlspecialchars(substr($value, $start)) . $this->highLightStyles
['ignored'][1];
1225 $lineC .= $this->highLightStyles
['error'][0] . '<strong> - ERROR:</strong> ' . htmlspecialchars(implode(';', $errA[$rawP])) . $this->highLightStyles
['error'][1];
1227 if ($highlightBlockMode && $this->highLightData_bracelevel
[$rawP]) {
1228 $lineC = str_pad('', $this->highLightData_bracelevel
[$rawP] * 2, ' ', STR_PAD_LEFT
) . '<span style="' . $this->highLightBlockStyles
. ($this->highLightBlockStyles_basecolor ?
'background-color: ' . GeneralUtility
::modifyHTMLColorAll($this->highLightBlockStyles_basecolor
, -$this->highLightData_bracelevel
[$rawP] * 16) : '') . '">' . ($lineC !== '' ?
$lineC : ' ') . '</span>';
1230 if (is_array($lineNumDat)) {
1231 $lineNum = $rawP +
$lineNumDat[0];
1232 if ($this->parentObject
instanceof \TYPO3\CMS\Core\TypoScript\ExtendedTemplateService
) {
1233 $lineNum = $this->parentObject
->ext_lnBreakPointWrap($lineNum, $lineNum);
1235 $lineC = $this->highLightStyles
['linenum'][0] . str_pad($lineNum, 4, ' ', STR_PAD_LEFT
) . ':' . $this->highLightStyles
['linenum'][1] . ' ' . $lineC;
1239 return '<pre class="ts-hl">' . implode(LF
, $lines) . '</pre>';