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