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