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