TYPO3  7.6
TypoScriptParser.php
Go to the documentation of this file.
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 
24 
29 {
35  public $strict = true;
36 
42  public $setup = array();
43 
49  public $raw;
50 
56  public $rawP;
57 
63  public $lastComment = '';
64 
70  public $commentSet = false;
71 
77  public $multiLineEnabled = false;
78 
84  public $multiLineObject = '';
85 
91  public $multiLineValue = array();
92 
98  public $inBrace = 0;
99 
106  public $lastConditionTrue = true;
107 
113  public $sections = array();
114 
120  public $sectionsMatch = array();
121 
127  public $syntaxHighLight = false;
128 
134  public $highLightData = array();
135 
141  public $highLightData_bracelevel = array();
142 
148  public $regComments = false;
149 
155  public $regLinenumbers = false;
156 
162  public $errors = array();
163 
169  public $lineNumberOffset = 0;
170 
176  public $breakPointLN = 0;
177 
181  public $highLightStyles = array(
182  'prespace' => array('<span class="ts-prespace">', '</span>'),
183  // Space before any content on a line
184  'objstr_postspace' => array('<span class="ts-objstr_postspace">', '</span>'),
185  // Space after the object string on a line
186  'operator_postspace' => array('<span class="ts-operator_postspace">', '</span>'),
187  // Space after the operator on a line
188  'operator' => array('<span class="ts-operator">', '</span>'),
189  // The operator char
190  'value' => array('<span class="ts-value">', '</span>'),
191  // The value of a line
192  'objstr' => array('<span class="ts-objstr">', '</span>'),
193  // The object string of a line
194  'value_copy' => array('<span class="ts-value_copy">', '</span>'),
195  // The value when the copy syntax (<) is used; that means the object reference
196  'value_unset' => array('<span class="ts-value_unset">', '</span>'),
197  // The value when an object is unset. Should not exist.
198  'ignored' => array('<span class="ts-ignored">', '</span>'),
199  // The "rest" of a line which will be ignored.
200  'default' => array('<span class="ts-default">', '</span>'),
201  // The default style if none other is applied.
202  'comment' => array('<span class="ts-comment">', '</span>'),
203  // Comment lines
204  'condition' => array('<span class="ts-condition">', '</span>'),
205  // Conditions
206  'error' => array('<span class="ts-error">', '</span>'),
207  // Error messages
208  'linenum' => array('<span class="ts-linenum">', '</span>')
209  );
210 
217 
224 
229 
238  public function parse($string, $matchObj = '')
239  {
240  $this->raw = explode(LF, $string);
241  $this->rawP = 0;
242  $pre = '[GLOBAL]';
243  while ($pre) {
244  if ($this->breakPointLN && $pre === '[_BREAK]') {
245  $this->error('Breakpoint at ' . ($this->lineNumberOffset + $this->rawP - 2) . ': Line content was "' . $this->raw[($this->rawP - 2)] . '"', 1);
246  break;
247  }
248  $preUppercase = strtoupper($pre);
249  if ($pre[0] === '[' &&
250  ($preUppercase === '[GLOBAL]' ||
251  $preUppercase === '[END]' ||
252  !$this->lastConditionTrue && $preUppercase === '[ELSE]')
253  ) {
254  $pre = trim($this->parseSub($this->setup));
255  $this->lastConditionTrue = 1;
256  } else {
257  // We're in a specific section. Therefore we log this section
258  $specificSection = $preUppercase !== '[ELSE]';
259  if ($specificSection) {
260  $this->sections[md5($pre)] = $pre;
261  }
262  if (is_object($matchObj) && $matchObj->match($pre) || $this->syntaxHighLight) {
263  if ($specificSection) {
264  $this->sectionsMatch[md5($pre)] = $pre;
265  }
266  $pre = trim($this->parseSub($this->setup));
267  $this->lastConditionTrue = 1;
268  } else {
269  $pre = $this->nextDivider();
270  $this->lastConditionTrue = 0;
271  }
272  }
273  }
274  if ($this->inBrace) {
275  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)', 1);
276  }
277  if ($this->multiLineEnabled) {
278  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': A multiline value section is not ended with a parenthesis!', 1);
279  }
280  $this->lineNumberOffset += count($this->raw) + 1;
281  }
282 
289  public function nextDivider()
290  {
291  while (isset($this->raw[$this->rawP])) {
292  $line = trim($this->raw[$this->rawP]);
293  $this->rawP++;
294  if ($line && $line[0] === '[') {
295  return $line;
296  }
297  }
298  return '';
299  }
300 
307  public function parseSub(array &$setup)
308  {
309  while (isset($this->raw[$this->rawP])) {
310  $line = ltrim($this->raw[$this->rawP]);
311  $lineP = $this->rawP;
312  $this->rawP++;
313  if ($this->syntaxHighLight) {
314  $this->regHighLight('prespace', $lineP, strlen($line));
315  }
316  // Breakpoint?
317  // By adding 1 we get that line processed
318  if ($this->breakPointLN && $this->lineNumberOffset + $this->rawP - 1 === $this->breakPointLN + 1) {
319  return '[_BREAK]';
320  }
321  // Set comment flag?
322  if (!$this->multiLineEnabled && strpos($line, '/*') === 0) {
323  $this->commentSet = 1;
324  }
325  // If $this->multiLineEnabled we will go and get the line values here because we know, the first if() will be TRUE.
326  if (!$this->commentSet && ($line || $this->multiLineEnabled)) {
327  // If multiline is enabled. Escape by ')'
328  if ($this->multiLineEnabled) {
329  // Multiline ends...
330  if ($line[0] === ')') {
331  if ($this->syntaxHighLight) {
332  $this->regHighLight('operator', $lineP, strlen($line) - 1);
333  }
334  // Disable multiline
335  $this->multiLineEnabled = 0;
336  $theValue = implode($this->multiLineValue, LF);
337  if (strpos($this->multiLineObject, '.') !== false) {
338  // Set the value deeper.
339  $this->setVal($this->multiLineObject, $setup, array($theValue));
340  } else {
341  // Set value regularly
342  $setup[$this->multiLineObject] = $theValue;
343  if ($this->lastComment && $this->regComments) {
344  $setup[$this->multiLineObject . '..'] .= $this->lastComment;
345  }
346  if ($this->regLinenumbers) {
347  $setup[$this->multiLineObject . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
348  }
349  }
350  } else {
351  if ($this->syntaxHighLight) {
352  $this->regHighLight('value', $lineP);
353  }
354  $this->multiLineValue[] = $this->raw[$this->rawP - 1];
355  }
356  } elseif ($this->inBrace === 0 && $line[0] === '[') {
357  // Beginning of condition (only on level zero compared to brace-levels
358  if ($this->syntaxHighLight) {
359  $this->regHighLight('condition', $lineP);
360  }
361  return $line;
362  } else {
363  // Return if GLOBAL condition is set - no matter what.
364  if ($line[0] === '[' && stripos($line, '[GLOBAL]') !== false) {
365  if ($this->syntaxHighLight) {
366  $this->regHighLight('condition', $lineP);
367  }
368  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': On return to [GLOBAL] scope, the script was short of ' . $this->inBrace . ' end brace(s)', 1);
369  $this->inBrace = 0;
370  return $line;
371  } elseif ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
372  // If not brace-end or comment
373  // Find object name string until we meet an operator
374  $varL = strcspn($line, TAB . ' {=<>(');
375  // check for special ":=" operator
376  if ($varL > 0 && substr($line, $varL-1, 2) === ':=') {
377  --$varL;
378  }
379  // also remove tabs after the object string name
380  $objStrName = substr($line, 0, $varL);
381  if ($this->syntaxHighLight) {
382  $this->regHighLight('objstr', $lineP, strlen(substr($line, $varL)));
383  }
384  if ($objStrName !== '') {
385  $r = array();
386  if ($this->strict && preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
387  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."');
388  } else {
389  $line = ltrim(substr($line, $varL));
390  if ($this->syntaxHighLight) {
391  $this->regHighLight('objstr_postspace', $lineP, strlen($line));
392  if ($line !== '') {
393  $this->regHighLight('operator', $lineP, strlen($line) - 1);
394  $this->regHighLight('operator_postspace', $lineP, strlen(ltrim(substr($line, 1))));
395  }
396  }
397  if ($line === '') {
398  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
399  } else {
400  // Checking for special TSparser properties (to change TS values at parsetime)
401  $match = array();
402  if ($line[0] === ':' && preg_match('/^:=\\s*([[:alpha:]]+)\\s*\\((.*)\\).*/', $line, $match)) {
403  $tsFunc = $match[1];
404  $tsFuncArg = $match[2];
405  list($currentValue) = $this->getVal($objStrName, $setup);
406  $tsFuncArg = str_replace(array('\\\\', '\\n', '\\t'), array('\\', LF, TAB), $tsFuncArg);
407  $newValue = $this->executeValueModifier($tsFunc, $tsFuncArg, $currentValue);
408  if (isset($newValue)) {
409  $line = '= ' . $newValue;
410  }
411  }
412  switch ($line[0]) {
413  case '=':
414  if ($this->syntaxHighLight) {
415  $this->regHighLight('value', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
416  }
417  if (strpos($objStrName, '.') !== false) {
418  $value = array();
419  $value[0] = trim(substr($line, 1));
420  $this->setVal($objStrName, $setup, $value);
421  } else {
422  $setup[$objStrName] = trim(substr($line, 1));
423  if ($this->lastComment && $this->regComments) {
424  // Setting comment..
425  $setup[$objStrName . '..'] .= $this->lastComment;
426  }
427  if ($this->regLinenumbers) {
428  $setup[$objStrName . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
429  }
430  }
431  break;
432  case '{':
433  $this->inBrace++;
434  if (strpos($objStrName, '.') !== false) {
435  $exitSig = $this->rollParseSub($objStrName, $setup);
436  if ($exitSig) {
437  return $exitSig;
438  }
439  } else {
440  if (!isset($setup[($objStrName . '.')])) {
441  $setup[$objStrName . '.'] = array();
442  }
443  $exitSig = $this->parseSub($setup[$objStrName . '.']);
444  if ($exitSig) {
445  return $exitSig;
446  }
447  }
448  break;
449  case '(':
450  $this->multiLineObject = $objStrName;
451  $this->multiLineEnabled = 1;
452  $this->multiLineValue = array();
453  break;
454  case '<':
455  if ($this->syntaxHighLight) {
456  $this->regHighLight('value_copy', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
457  }
458  $theVal = trim(substr($line, 1));
459  if ($theVal[0] === '.') {
460  $res = $this->getVal(substr($theVal, 1), $setup);
461  } else {
462  $res = $this->getVal($theVal, $this->setup);
463  }
464  $this->setVal($objStrName, $setup, unserialize(serialize($res)), 1);
465  // unserialize(serialize(...)) may look stupid but is needed because of some reference issues. See Kaspers reply to "[TYPO3-core] good question" from December 15 2005.
466  break;
467  case '>':
468  if ($this->syntaxHighLight) {
469  $this->regHighLight('value_unset', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
470  }
471  $this->setVal($objStrName, $setup, 'UNSET');
472  break;
473  default:
474  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
475  }
476  }
477  }
478  $this->lastComment = '';
479  }
480  } elseif ($line[0] === '}') {
481  $this->inBrace--;
482  $this->lastComment = '';
483  if ($this->syntaxHighLight) {
484  $this->regHighLight('operator', $lineP, strlen($line) - 1);
485  }
486  if ($this->inBrace < 0) {
487  $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': An end brace is in excess.', 1);
488  $this->inBrace = 0;
489  } else {
490  break;
491  }
492  } else {
493  if ($this->syntaxHighLight) {
494  $this->regHighLight('comment', $lineP);
495  }
496  // Comment. The comments are concatenated in this temporary string:
497  if ($this->regComments) {
498  $this->lastComment .= rtrim($line) . LF;
499  }
500  }
501  if (StringUtility::beginsWith($line, '### ERROR')) {
502  $this->error(substr($line, 11));
503  }
504  }
505  }
506  // Unset comment
507  if ($this->commentSet) {
508  if ($this->syntaxHighLight) {
509  $this->regHighLight('comment', $lineP);
510  }
511  if (strpos($line, '*/') === 0) {
512  $this->commentSet = 0;
513  }
514  }
515  }
516  return null;
517  }
518 
528  protected function executeValueModifier($modifierName, $modifierArgument = null, $currentValue = null)
529  {
530  $newValue = null;
531  switch ($modifierName) {
532  case 'prependString':
533  $newValue = $modifierArgument . $currentValue;
534  break;
535  case 'appendString':
536  $newValue = $currentValue . $modifierArgument;
537  break;
538  case 'removeString':
539  $newValue = str_replace($modifierArgument, '', $currentValue);
540  break;
541  case 'replaceString':
542  list($fromStr, $toStr) = explode('|', $modifierArgument, 2);
543  $newValue = str_replace($fromStr, $toStr, $currentValue);
544  break;
545  case 'addToList':
546  $newValue = ((string)$currentValue !== '' ? $currentValue . ',' : '') . $modifierArgument;
547  break;
548  case 'removeFromList':
549  $existingElements = GeneralUtility::trimExplode(',', $currentValue);
550  $removeElements = GeneralUtility::trimExplode(',', $modifierArgument);
551  if (!empty($removeElements)) {
552  $newValue = implode(',', array_diff($existingElements, $removeElements));
553  }
554  break;
555  case 'uniqueList':
556  $elements = GeneralUtility::trimExplode(',', $currentValue);
557  $newValue = implode(',', array_unique($elements));
558  break;
559  case 'reverseList':
560  $elements = GeneralUtility::trimExplode(',', $currentValue);
561  $newValue = implode(',', array_reverse($elements));
562  break;
563  case 'sortList':
564  $elements = GeneralUtility::trimExplode(',', $currentValue);
565  $arguments = GeneralUtility::trimExplode(',', $modifierArgument);
566  $arguments = array_map('strtolower', $arguments);
567  $sort_flags = SORT_REGULAR;
568  if (in_array('numeric', $arguments)) {
569  $sort_flags = SORT_NUMERIC;
570  // If the sorting modifier "numeric" is given, all values
571  // are checked and an exception is thrown if a non-numeric value is given
572  // otherwise there is a different behaviour between PHP7 and PHP 5.x
573  // See also the warning on http://us.php.net/manual/en/function.sort.php
574  foreach ($elements as $element) {
575  if (!is_numeric($element)) {
576  throw new \InvalidArgumentException('The list "' . $currentValue . '" should be sorted numerically but contains a non-numeric value', 1438191758);
577  }
578  }
579  }
580  sort($elements, $sort_flags);
581  if (in_array('descending', $arguments)) {
582  $elements = array_reverse($elements);
583  }
584  $newValue = implode(',', $elements);
585  break;
586  default:
587  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName])) {
588  $hookMethod = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName];
589  $params = array('currentValue' => $currentValue, 'functionArgument' => $modifierArgument);
590  $fakeThis = false;
591  $newValue = GeneralUtility::callUserFunction($hookMethod, $params, $fakeThis);
592  } else {
593  GeneralUtility::sysLog(
594  'Missing function definition for ' . $modifierName . ' on TypoScript',
595  'core',
597  );
598  }
599  }
600  return $newValue;
601  }
602 
612  public function rollParseSub($string, array &$setup)
613  {
614  if ((string)$string === '') {
615  return '';
616  }
617 
618  list($key, $remainingKey) = $this->parseNextKeySegment($string);
619  $key .= '.';
620  if (!isset($setup[$key])) {
621  $setup[$key] = array();
622  }
623  $exitSig = $remainingKey === ''
624  ? $this->parseSub($setup[$key])
625  : $this->rollParseSub($remainingKey, $setup[$key]);
626  return $exitSig ?: '';
627  }
628 
637  public function getVal($string, $setup)
638  {
639  if ((string)$string === '') {
640  return array();
641  }
642 
643  list($key, $remainingKey) = $this->parseNextKeySegment($string);
644  $subKey = $key . '.';
645  if ($remainingKey === '') {
646  $retArr = array();
647  if (isset($setup[$key])) {
648  $retArr[0] = $setup[$key];
649  }
650  if (isset($setup[$subKey])) {
651  $retArr[1] = $setup[$subKey];
652  }
653  return $retArr;
654  } else {
655  if ($setup[$subKey]) {
656  return $this->getVal($remainingKey, $setup[$subKey]);
657  }
658  }
659  return array();
660  }
661 
671  public function setVal($string, array &$setup, $value, $wipeOut = false)
672  {
673  if ((string)$string === '') {
674  return;
675  }
676 
677  list($key, $remainingKey) = $this->parseNextKeySegment($string);
678  $subKey = $key . '.';
679  if ($remainingKey === '') {
680  if ($value === 'UNSET') {
681  unset($setup[$key]);
682  unset($setup[$subKey]);
683  if ($this->regLinenumbers) {
684  $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '>';
685  }
686  } else {
687  $lnRegisDone = 0;
688  if ($wipeOut && $this->strict) {
689  unset($setup[$key]);
690  unset($setup[$subKey]);
691  if ($this->regLinenumbers) {
692  $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '<';
693  $lnRegisDone = 1;
694  }
695  }
696  if (isset($value[0])) {
697  $setup[$key] = $value[0];
698  }
699  if (isset($value[1])) {
700  $setup[$subKey] = $value[1];
701  }
702  if ($this->lastComment && $this->regComments) {
703  $setup[$key . '..'] .= $this->lastComment;
704  }
705  if ($this->regLinenumbers && !$lnRegisDone) {
706  $setup[$key . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
707  }
708  }
709  } else {
710  if (!isset($setup[$subKey])) {
711  $setup[$subKey] = array();
712  }
713  $this->setVal($remainingKey, $setup[$subKey], $value);
714  }
715  }
716 
728  protected function parseNextKeySegment($key)
729  {
730  // if no dot is in the key, nothing to do
731  $dotPosition = strpos($key, '.');
732  if ($dotPosition === false) {
733  return array($key, '');
734  }
735 
736  if (strpos($key, '\\') !== false) {
737  // backslashes are in the key, so we do further parsing
738 
739  while ($dotPosition !== false) {
740  if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
741  break;
742  }
743  // escaped dot found, continue
744  $dotPosition = strpos($key, '.', $dotPosition + 1);
745  }
746 
747  if ($dotPosition === false) {
748  // no regular dot found
749  $keySegment = $key;
750  $remainingKey = '';
751  } else {
752  if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
753  $keySegment = substr($key, 0, $dotPosition - 1);
754  } else {
755  $keySegment = substr($key, 0, $dotPosition);
756  }
757  $remainingKey = substr($key, $dotPosition + 1);
758  }
759 
760  // fix key segment by removing escape sequences
761  $keySegment = str_replace('\\.', '.', $keySegment);
762  } else {
763  // no backslash in the key, we're fine off
764  list($keySegment, $remainingKey) = explode('.', $key, 2);
765  }
766  return array($keySegment, $remainingKey);
767  }
768 
777  public function error($err, $num = 2)
778  {
779  $tt = $this->getTimeTracker();
780  if ($tt !== null) {
781  $tt->setTSlogMessage($err, $num);
782  }
783  $this->errors[] = array($err, $num, $this->rawP - 1, $this->lineNumberOffset);
784  }
785 
797  public static function checkIncludeLines($string, $cycle_counter = 1, $returnFiles = false, $parentFilenameOrPath = '')
798  {
799  $includedFiles = array();
800  if ($cycle_counter > 100) {
801  GeneralUtility::sysLog('It appears like TypoScript code is looping over itself. Check your templates for "&lt;INCLUDE_TYPOSCRIPT: ..." tags', 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
802  if ($returnFiles) {
803  return array(
804  'typoscript' => '',
805  'files' => $includedFiles
806  );
807  }
808  return '
809 ###
810 ### ERROR: Recursion!
811 ###
812 ';
813  }
814 
815  // If no tags found, no need to do slower preg_split
816  if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== false) {
817  $splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
818  $parts = preg_split($splitRegEx, LF . $string . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
819  // First text part goes through
820  $newString = $parts[0] . LF;
821  $partCount = count($parts);
822  for ($i = 1; $i + 3 < $partCount; $i += 4) {
823  // $parts[$i] contains 'FILE' or 'DIR'
824  // $parts[$i+1] contains relative file or directory path to be included
825  // $parts[$i+2] optional properties of the INCLUDE statement
826  // $parts[$i+3] next part of the typoscript string (part in between include-tags)
827  $includeType = $parts[$i];
828  $filename = $parts[$i + 1];
829  $originalFilename = $filename;
830  $optionalProperties = $parts[$i + 2];
831  $tsContentsTillNextInclude = $parts[$i + 3];
832 
833  // Check condition
834  $matches = preg_split('#(?i)condition\\s*=\\s*"((?:\\\\\\\\|\\\\"|[^\\"])*)"(\\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
835  // If there was a condition
836  if (count($matches) > 1) {
837  // Unescape the condition
838  $condition = trim(stripslashes($matches[1]));
839  // If necessary put condition in square brackets
840  if ($condition[0] !== '[') {
841  $condition = '[' . $condition . ']';
842  }
844  $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
845  // If it didn't match then proceed to the next include
846  if (!$conditionMatcher->match($condition)) {
847  continue;
848  }
849  }
850 
851  // Resolve a possible relative paths if a parent file is given
852  if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
854  }
855 
856  // There must be a line-break char after - not sure why this check is necessary, kept it for being 100% backwards compatible
857  // An empty string is also ok (means that the next line is also a valid include_typoscript tag)
858  if (!preg_match('/(^\\s*\\r?\\n|^$)/', $tsContentsTillNextInclude)) {
859  $newString .= self::typoscriptIncludeError('Invalid characters after <INCLUDE_TYPOSCRIPT: source="' . $includeType . ':' . $filename . '">-tag (rest of line must be empty).');
860  } elseif (strpos('..', $filename) !== false) {
861  $newString .= self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
862  } else {
863  switch (strtolower($includeType)) {
864  case 'file':
865  self::includeFile($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
866  break;
867  case 'dir':
868  self::includeDirectory($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
869  break;
870  default:
871  $newString .= self::typoscriptIncludeError('No valid option for INCLUDE_TYPOSCRIPT source property (valid options are FILE or DIR)');
872  }
873  }
874  // Prepend next normal (not file) part to output string
875  $newString .= $tsContentsTillNextInclude . LF;
876 
877  // load default TypoScript for content rendering templates like
878  // css_styled_content if those have been included through f.e.
879  // <INCLUDE_TYPOSCRIPT: source="FILE:EXT:css_styled_content/static/setup.txt">
880  $filePointer = strtolower($filename);
881  if (StringUtility::beginsWith($filePointer, 'ext:')) {
882  $filePointerPathParts = explode('/', substr($filePointer, 4));
883 
884  // remove file part, determine whether to load setup or constants
885  list($includeType, ) = explode('.', array_pop($filePointerPathParts));
886 
887  if (in_array($includeType, array('setup', 'constants'))) {
888  // adapt extension key to required format (no underscores)
889  $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
890 
891  // load default TypoScript
892  $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
893  if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
894  $newString .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
895  }
896  }
897  }
898  }
899  // Add a line break before and after the included code in order to make sure that the parser always has a LF.
900  $string = LF . trim($newString) . LF;
901  }
902  // When all included files should get returned, simply return an compound array containing
903  // the TypoScript with all "includes" processed and the files which got included
904  if ($returnFiles) {
905  return array(
906  'typoscript' => $string,
907  'files' => $includedFiles
908  );
909  }
910  return $string;
911  }
912 
926  public static function includeFile($filename, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = array(), $optionalProperties = '', $parentFilenameOrPath = '')
927  {
928  // Resolve a possible relative paths if a parent file is given
929  if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
930  $absfilename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
931  } else {
932  $absfilename = $filename;
933  }
934  $absfilename = GeneralUtility::getFileAbsFileName($absfilename);
935 
936  $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> BEGIN:' . LF;
937  if ((string)$filename !== '') {
938  // Must exist and must not contain '..' and must be relative
939  // Check for allowed files
941  $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
942  } elseif (!@file_exists($absfilename)) {
943  $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not found.');
944  } else {
945  $includedFiles[] = $absfilename;
946  // check for includes in included text
947  $included_text = self::checkIncludeLines(GeneralUtility::getUrl($absfilename), $cycle_counter + 1, $returnFiles, $absfilename);
948  // If the method also has to return all included files, merge currently included
949  // files with files included by recursively calling itself
950  if ($returnFiles && is_array($included_text)) {
951  $includedFiles = array_merge($includedFiles, $included_text['files']);
952  $included_text = $included_text['typoscript'];
953  }
954  $newString .= $included_text . LF;
955  }
956  }
957  $newString .= '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> END:' . LF . LF;
958  }
959 
975  protected static function includeDirectory($dirPath, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = array(), $optionalProperties = '', $parentFilenameOrPath = '')
976  {
977  // Extract the value of the property extensions="..."
978  $matches = preg_split('#(?i)extensions\s*=\s*"([^"]*)"(\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
979  if (count($matches) > 1) {
980  $includedFileExtensions = $matches[1];
981  } else {
982  $includedFileExtensions = '';
983  }
984 
985  // Resolve a possible relative paths if a parent file is given
986  if ($parentFilenameOrPath !== '' && $dirPath[0] === '.') {
987  $resolvedDirPath = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $dirPath);
988  } else {
989  $resolvedDirPath = $dirPath;
990  }
991  $absDirPath = GeneralUtility::getFileAbsFileName($resolvedDirPath);
992  if ($absDirPath) {
993  $absDirPath = rtrim($absDirPath, '/') . '/';
994  $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> BEGIN:' . LF;
995  // Get alphabetically sorted file index in array
996  $fileIndex = GeneralUtility::getAllFilesAndFoldersInPath(array(), $absDirPath, $includedFileExtensions);
997  // Prepend file contents to $newString
998  $prefixLength = strlen(PATH_site);
999  foreach ($fileIndex as $absFileRef) {
1000  $relFileRef = substr($absFileRef, $prefixLength);
1001  self::includeFile($relFileRef, $cycle_counter, $returnFiles, $newString, $includedFiles, '', $absDirPath);
1002  }
1003  $newString .= '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> END:' . LF . LF;
1004  } else {
1005  $newString .= self::typoscriptIncludeError('The path "' . $resolvedDirPath . '" is invalid.');
1006  }
1007  }
1008 
1017  protected static function typoscriptIncludeError($error)
1018  {
1019  GeneralUtility::sysLog($error, 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
1020  return "\n###\n### ERROR: " . $error . "\n###\n\n";
1021  }
1022 
1029  public static function checkIncludeLines_array(array $array)
1030  {
1031  foreach ($array as $k => $v) {
1032  $array[$k] = self::checkIncludeLines($array[$k]);
1033  }
1034  return $array;
1035  }
1036 
1050  public static function extractIncludes($string, $cycle_counter = 1, array $extractedFileNames = array(), $parentFilenameOrPath = '')
1051  {
1052  if ($cycle_counter > 10) {
1053  GeneralUtility::sysLog('It appears like TypoScript code is looping over itself. Check your templates for "&lt;INCLUDE_TYPOSCRIPT: ..." tags', 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
1054  return '
1055 ###
1056 ### ERROR: Recursion!
1057 ###
1058 ';
1059  }
1060  $expectedEndTag = '';
1061  $fileContent = array();
1062  $restContent = array();
1063  $fileName = null;
1064  $inIncludePart = false;
1065  $lines = preg_split("/\r\n|\n|\r/", $string);
1066  $skipNextLineIfEmpty = false;
1067  $openingCommentedIncludeStatement = null;
1068  $optionalProperties = '';
1069  foreach ($lines as $line) {
1070  // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1071  // an additional empty line, remove this again
1072  if ($skipNextLineIfEmpty) {
1073  if (trim($line) === '') {
1074  continue;
1075  }
1076  $skipNextLineIfEmpty = false;
1077  }
1078 
1079  // Outside commented include statements
1080  if (!$inIncludePart) {
1081  // Search for beginning commented include statements
1082  if (preg_match('/###\\s*<INCLUDE_TYPOSCRIPT:\\s*source\\s*=\\s*"\\s*((?i)file|dir)\\s*:\\s*([^"]*)"(.*)>\\s*BEGIN/i', $line, $matches)) {
1083  // Found a commented include statement
1084 
1085  // Save this line in case there is no ending tag
1086  $openingCommentedIncludeStatement = trim($line);
1087  $openingCommentedIncludeStatement = preg_replace('/\\s*### Warning: .*###\\s*/', '', $openingCommentedIncludeStatement);
1088 
1089  // type of match: FILE or DIR
1090  $inIncludePart = strtoupper($matches[1]);
1091  $fileName = $matches[2];
1092  $optionalProperties = $matches[3];
1093 
1094  $expectedEndTag = '### <INCLUDE_TYPOSCRIPT: source="' . $inIncludePart . ':' . $fileName . '"' . $optionalProperties . '> END';
1095  // Strip all whitespace characters to make comparison safer
1096  $expectedEndTag = strtolower(preg_replace('/\s/', '', $expectedEndTag));
1097  } else {
1098  // If this is not a beginning commented include statement this line goes into the rest content
1099  $restContent[] = $line;
1100  }
1101  //if (is_array($matches)) GeneralUtility::devLog('matches', 'TypoScriptParser', 0, $matches);
1102  } else {
1103  // Inside commented include statements
1104  // Search for the matching ending commented include statement
1105  $strippedLine = preg_replace('/\s/', '', $line);
1106  if (stripos($strippedLine, $expectedEndTag) !== false) {
1107  // Found the matching ending include statement
1108  $fileContentString = implode(PHP_EOL, $fileContent);
1109 
1110  // Write the content to the file
1111 
1112  // Resolve a possible relative paths if a parent file is given
1113  if ($parentFilenameOrPath !== '' && $fileName[0] === '.') {
1114  $realFileName = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $fileName);
1115  } else {
1116  $realFileName = $fileName;
1117  }
1118  $realFileName = GeneralUtility::getFileAbsFileName($realFileName);
1119 
1120  if ($inIncludePart === 'FILE') {
1121  // Some file checks
1123  throw new \UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
1124  }
1125  if (empty($realFileName)) {
1126  throw new \UnexpectedValueException(sprintf('"%s" is not a valid file location.', $fileName), 1294586441);
1127  }
1128  if (!is_writable($realFileName)) {
1129  throw new \RuntimeException(sprintf('"%s" is not writable.', $fileName), 1294586442);
1130  }
1131  if (in_array($realFileName, $extractedFileNames)) {
1132  throw new \RuntimeException(sprintf('Recursive/multiple inclusion of file "%s"', $realFileName), 1294586443);
1133  }
1134  $extractedFileNames[] = $realFileName;
1135 
1136  // Recursive call to detected nested commented include statements
1137  $fileContentString = self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1138 
1139  // Write the content to the file
1140  if (!GeneralUtility::writeFile($realFileName, $fileContentString)) {
1141  throw new \RuntimeException(sprintf('Could not write file "%s"', $realFileName), 1294586444);
1142  }
1143  // Insert reference to the file in the rest content
1144  $restContent[] = '<INCLUDE_TYPOSCRIPT: source="FILE:' . $fileName . '"' . $optionalProperties . '>';
1145  } else {
1146  // must be DIR
1147 
1148  // Some file checks
1149  if (empty($realFileName)) {
1150  throw new \UnexpectedValueException(sprintf('"%s" is not a valid location.', $fileName), 1366493602);
1151  }
1152  if (!is_dir($realFileName)) {
1153  throw new \RuntimeException(sprintf('"%s" is not a directory.', $fileName), 1366493603);
1154  }
1155  if (in_array($realFileName, $extractedFileNames)) {
1156  throw new \RuntimeException(sprintf('Recursive/multiple inclusion of directory "%s"', $realFileName), 1366493604);
1157  }
1158  $extractedFileNames[] = $realFileName;
1159 
1160  // Recursive call to detected nested commented include statements
1161  self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1162 
1163  // just drop content between tags since it should usually just contain individual files from that dir
1164 
1165  // Insert reference to the dir in the rest content
1166  $restContent[] = '<INCLUDE_TYPOSCRIPT: source="DIR:' . $fileName . '"' . $optionalProperties . '>';
1167  }
1168 
1169  // Reset variables (preparing for the next commented include statement)
1170  $fileContent = array();
1171  $fileName = null;
1172  $inIncludePart = false;
1173  $openingCommentedIncludeStatement = null;
1174  // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1175  // an additional empty line, remove this again
1176  $skipNextLineIfEmpty = true;
1177  } else {
1178  // If this is not an ending commented include statement this line goes into the file content
1179  $fileContent[] = $line;
1180  }
1181  }
1182  }
1183  // If we're still inside commented include statements copy the lines back to the rest content
1184  if ($inIncludePart) {
1185  $restContent[] = $openingCommentedIncludeStatement . ' ### Warning: Corresponding end line missing! ###';
1186  $restContent = array_merge($restContent, $fileContent);
1187  }
1188  $restContentString = implode(PHP_EOL, $restContent);
1189  return $restContentString;
1190  }
1191 
1198  public static function extractIncludes_array(array $array)
1199  {
1200  foreach ($array as $k => $v) {
1201  $array[$k] = self::extractIncludes($array[$k]);
1202  }
1203  return $array;
1204  }
1205 
1206  /**********************************
1207  *
1208  * Syntax highlighting
1209  *
1210  *********************************/
1220  public function doSyntaxHighlight($string, $lineNum = '', $highlightBlockMode = false)
1221  {
1222  $this->syntaxHighLight = 1;
1223  $this->highLightData = array();
1224  $this->errors = array();
1225  // This is done in order to prevent empty <span>..</span> sections around CR content. Should not do anything but help lessen the amount of HTML code.
1226  $string = str_replace(CR, '', $string);
1227  $this->parse($string);
1228  return $this->syntaxHighlight_print($lineNum, $highlightBlockMode);
1229  }
1230 
1241  public function regHighLight($code, $pointer, $strlen = -1)
1242  {
1243  if ($strlen === -1) {
1244  $this->highLightData[$pointer] = array(array($code, 0));
1245  } else {
1246  $this->highLightData[$pointer][] = array($code, $strlen);
1247  }
1248  $this->highLightData_bracelevel[$pointer] = $this->inBrace;
1249  }
1250 
1260  public function syntaxHighlight_print($lineNumDat, $highlightBlockMode)
1261  {
1262  // Registers all error messages in relation to their linenumber
1263  $errA = array();
1264  foreach ($this->errors as $err) {
1265  $errA[$err[2]][] = $err[0];
1266  }
1267  // Generates the syntax highlighted output:
1268  $lines = array();
1269  foreach ($this->raw as $rawP => $value) {
1270  $start = 0;
1271  $strlen = strlen($value);
1272  $lineC = '';
1273  if (is_array($this->highLightData[$rawP])) {
1274  foreach ($this->highLightData[$rawP] as $set) {
1275  $len = $strlen - $start - $set[1];
1276  if ($len > 0) {
1277  $part = substr($value, $start, $len);
1278  $start += $len;
1279  $st = $this->highLightStyles[isset($this->highLightStyles[$set[0]]) ? $set[0] : 'default'];
1280  if (!$highlightBlockMode || $set[0] !== 'prespace') {
1281  $lineC .= $st[0] . htmlspecialchars($part) . $st[1];
1282  }
1283  } elseif ($len < 0) {
1284  debug(array($len, $value, $rawP));
1285  }
1286  }
1287  } else {
1288  debug(array($value));
1289  }
1290  if (strlen($value) > $start) {
1291  $lineC .= $this->highLightStyles['ignored'][0] . htmlspecialchars(substr($value, $start)) . $this->highLightStyles['ignored'][1];
1292  }
1293  if ($errA[$rawP]) {
1294  $lineC .= $this->highLightStyles['error'][0] . '<strong> - ERROR:</strong> ' . htmlspecialchars(implode(';', $errA[$rawP])) . $this->highLightStyles['error'][1];
1295  }
1296  if ($highlightBlockMode && $this->highLightData_bracelevel[$rawP]) {
1297  $lineC = str_pad('', $this->highLightData_bracelevel[$rawP] * 2, ' ', STR_PAD_LEFT) . '<span style="' . $this->highLightBlockStyles . ($this->highLightBlockStyles_basecolor ? 'background-color: ' . $this->modifyHTMLColorAll($this->highLightBlockStyles_basecolor, -$this->highLightData_bracelevel[$rawP] * 16) : '') . '">' . ($lineC !== '' ? $lineC : '&nbsp;') . '</span>';
1298  }
1299  if (is_array($lineNumDat)) {
1300  $lineNum = $rawP + $lineNumDat[0];
1301  if ($this->parentObject instanceof ExtendedTemplateService) {
1302  $lineNum = $this->parentObject->ext_lnBreakPointWrap($lineNum, $lineNum);
1303  }
1304  $lineC = $this->highLightStyles['linenum'][0] . str_pad($lineNum, 4, ' ', STR_PAD_LEFT) . ':' . $this->highLightStyles['linenum'][1] . ' ' . $lineC;
1305  }
1306  $lines[] = $lineC;
1307  }
1308  return '<pre class="ts-hl">' . implode(LF, $lines) . '</pre>';
1309  }
1310 
1314  protected function getTimeTracker()
1315  {
1316  return isset($GLOBALS['TT']) ? $GLOBALS['TT'] : null;
1317  }
1318 
1319 
1330  protected function modifyHTMLColor($color, $R, $G, $B)
1331  {
1332  // This takes a hex-color (# included!) and adds $R, $G and $B to the HTML-color (format: #xxxxxx) and returns the new color
1333  $nR = MathUtility::forceIntegerInRange(hexdec(substr($color, 1, 2)) + $R, 0, 255);
1334  $nG = MathUtility::forceIntegerInRange(hexdec(substr($color, 3, 2)) + $G, 0, 255);
1335  $nB = MathUtility::forceIntegerInRange(hexdec(substr($color, 5, 2)) + $B, 0, 255);
1336  return '#' . substr(('0' . dechex($nR)), -2) . substr(('0' . dechex($nG)), -2) . substr(('0' . dechex($nB)), -2);
1337  }
1338 
1347  protected function modifyHTMLColorAll($color, $all)
1348  {
1349  return $this->modifyHTMLColor($color, $all, $all, $all);
1350  }
1351 }