Passed
Push — master ( 3e02b0...e0a47f )
by
unknown
16:25
created

TypoScriptParser::regHighLight()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 3
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\TypoScript\Parser;
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use Psr\Log\LoggerInterface;
20
use Symfony\Component\Finder\Finder;
21
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as BackendConditionMatcher;
22
use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
23
use TYPO3\CMS\Core\Core\Environment;
24
use TYPO3\CMS\Core\Http\ApplicationType;
25
use TYPO3\CMS\Core\Log\LogManager;
26
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
27
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\PathUtility;
30
use TYPO3\CMS\Core\Utility\StringUtility;
31
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher;
32
33
/**
34
 * The TypoScript parser
35
 */
36
class TypoScriptParser
37
{
38
    /**
39
     * TypoScript hierarchy being build during parsing.
40
     *
41
     * @var array
42
     */
43
    public $setup = [];
44
45
    /**
46
     * Raw data, the input string exploded by LF
47
     *
48
     * @var string[]
49
     */
50
    protected $raw;
51
52
    /**
53
     * Pointer to entry in raw data array
54
     *
55
     * @var int
56
     */
57
    protected $rawP;
58
59
    /**
60
     * Holding the value of the last comment
61
     *
62
     * @var string
63
     */
64
    protected $lastComment = '';
65
66
    /**
67
     * Internally set, used as internal flag to create a multi-line comment (one of those like /* ... * /
68
     *
69
     * @var bool
70
     */
71
    protected $commentSet = false;
72
73
    /**
74
     * Internally set, when multiline value is accumulated
75
     *
76
     * @var bool
77
     */
78
    protected $multiLineEnabled = false;
79
80
    /**
81
     * Internally set, when multiline value is accumulated
82
     *
83
     * @var string
84
     */
85
    protected $multiLineObject = '';
86
87
    /**
88
     * Internally set, when multiline value is accumulated
89
     *
90
     * @var array
91
     */
92
    protected $multiLineValue = [];
93
94
    /**
95
     * Internally set, when in brace. Counter.
96
     *
97
     * @var int
98
     */
99
    protected $inBrace = 0;
100
101
    /**
102
     * For each condition this flag is set, if the condition is TRUE,
103
     * else it's cleared. Then it's used by the [ELSE] condition to determine if the next part should be parsed.
104
     *
105
     * @var bool
106
     */
107
    protected $lastConditionTrue = true;
108
109
    /**
110
     * Tracking all conditions found
111
     *
112
     * @var array
113
     */
114
    public $sections = [];
115
116
    /**
117
     * Tracking all matching conditions found
118
     *
119
     * @var array
120
     */
121
    public $sectionsMatch = [];
122
123
    /**
124
     * DO NOT register the comments. This is default for the ordinary sitetemplate!
125
     *
126
     * @var bool
127
     */
128
    public $regComments = false;
129
130
    /**
131
     * DO NOT register the linenumbers. This is default for the ordinary sitetemplate!
132
     *
133
     * @var bool
134
     */
135
    public $regLinenumbers = false;
136
137
    /**
138
     * Error accumulation array.
139
     *
140
     * @var array
141
     */
142
    public $errors = [];
143
144
    /**
145
     * Used for the error messages line number reporting. Set externally.
146
     *
147
     * @var int
148
     */
149
    public $lineNumberOffset = 0;
150
151
    /**
152
     * @deprecated Unused since v11, will be removed in v12
153
     */
154
    public $breakPointLN = 0;
155
156
    /**
157
     * @deprecated Unused since v11, will be removed in v12
158
     */
159
    public $parentObject;
160
161
    /**
162
     * Start parsing the input TypoScript text piece. The result is stored in $this->setup
163
     *
164
     * @param string $string The TypoScript text
165
     * @param object|string $matchObj If is object, then this is used to match conditions found in the TypoScript code. If matchObj not specified, then no conditions will work! (Except [GLOBAL])
166
     */
167
    public function parse($string, $matchObj = '')
168
    {
169
        $this->raw = explode(LF, $string);
170
        $this->rawP = 0;
171
        $pre = '[GLOBAL]';
172
        while ($pre) {
173
            if ($pre === '[]') {
174
                $this->error('Empty condition is always false, this does not make sense. At line ' . ($this->lineNumberOffset + $this->rawP - 1), 2);
175
                break;
176
            }
177
            $preUppercase = strtoupper($pre);
178
            if ($pre[0] === '[' &&
179
                ($preUppercase === '[GLOBAL]' ||
180
                    $preUppercase === '[END]' ||
181
                    !$this->lastConditionTrue && $preUppercase === '[ELSE]')
182
            ) {
183
                $pre = trim($this->parseSub($this->setup));
184
                $this->lastConditionTrue = true;
185
            } else {
186
                // We're in a specific section. Therefore we log this section
187
                $specificSection = $preUppercase !== '[ELSE]';
188
                if ($specificSection) {
189
                    $this->sections[md5($pre)] = $pre;
190
                }
191
                if (is_object($matchObj) && $matchObj->match($pre)) {
192
                    if ($specificSection) {
193
                        $this->sectionsMatch[md5($pre)] = $pre;
194
                    }
195
                    $pre = trim($this->parseSub($this->setup));
196
                    $this->lastConditionTrue = true;
197
                } else {
198
                    $pre = $this->nextDivider();
199
                    $this->lastConditionTrue = false;
200
                }
201
            }
202
        }
203
        if ($this->inBrace) {
204
            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)', 1);
205
        }
206
        if ($this->multiLineEnabled) {
207
            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': A multiline value section is not ended with a parenthesis!', 1);
208
        }
209
        $this->lineNumberOffset += count($this->raw) + 1;
210
    }
211
212
    /**
213
     * Will search for the next condition. When found it will return the line content (the condition value) and have advanced the internal $this->rawP pointer to point to the next line after the condition.
214
     *
215
     * @return string The condition value
216
     * @see parse()
217
     */
218
    protected function nextDivider()
219
    {
220
        while (isset($this->raw[$this->rawP])) {
221
            $line = trim($this->raw[$this->rawP]);
222
            $this->rawP++;
223
            if ($line && $line[0] === '[') {
224
                return $line;
225
            }
226
        }
227
        return '';
228
    }
229
230
    /**
231
     * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
232
     *
233
     * @param array $setup Reference to the setup array in which to accumulate the values.
234
     * @return string Returns the string of the condition found, the exit signal or possible nothing (if it completed parsing with no interruptions)
235
     */
236
    protected function parseSub(array &$setup)
237
    {
238
        while (isset($this->raw[$this->rawP])) {
239
            $line = ltrim($this->raw[$this->rawP]);
240
            $lineP = $this->rawP;
0 ignored issues
show
Unused Code introduced by
The assignment to $lineP is dead and can be removed.
Loading history...
241
            $this->rawP++;
242
            // Set comment flag?
243
            if (!$this->multiLineEnabled && strpos($line, '/*') === 0) {
244
                $this->commentSet = true;
245
            }
246
            // If $this->multiLineEnabled we will go and get the line values here because we know, the first if() will be TRUE.
247
            if (!$this->commentSet && ($line || $this->multiLineEnabled)) {
248
                // If multiline is enabled. Escape by ')'
249
                if ($this->multiLineEnabled) {
250
                    // Multiline ends...
251
                    if (!empty($line[0]) && $line[0] === ')') {
252
                        // Disable multiline
253
                        $this->multiLineEnabled = false;
254
                        $theValue = implode(LF, $this->multiLineValue);
255
                        if (strpos($this->multiLineObject, '.') !== false) {
256
                            // Set the value deeper.
257
                            $this->setVal($this->multiLineObject, $setup, [$theValue]);
258
                        } else {
259
                            // Set value regularly
260
                            $setup[$this->multiLineObject] = $theValue;
261
                            if ($this->lastComment && $this->regComments) {
262
                                $setup[$this->multiLineObject . '..'] .= $this->lastComment;
263
                            }
264
                            if ($this->regLinenumbers) {
265
                                $setup[$this->multiLineObject . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
266
                            }
267
                        }
268
                    } else {
269
                        $this->multiLineValue[] = $this->raw[$this->rawP - 1];
270
                    }
271
                } elseif ($this->inBrace === 0 && $line[0] === '[') {
272
                    if (substr(trim($line), -1, 1) !== ']') {
273
                        $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Invalid condition found, any condition must end with "]": ' . $line);
274
                        return $line;
275
                    }
276
                    return $line;
277
                } else {
278
                    // Return if GLOBAL condition is set - no matter what.
279
                    if ($line[0] === '[' && stripos($line, '[GLOBAL]') !== false) {
280
                        $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': On return to [GLOBAL] scope, the script was short of ' . $this->inBrace . ' end brace(s)', 1);
281
                        $this->inBrace = 0;
282
                        return $line;
283
                    }
284
                    if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
285
                        // If not brace-end or comment
286
                        // Find object name string until we meet an operator
287
                        $varL = strcspn($line, "\t" . ' {=<>(');
288
                        // check for special ":=" operator
289
                        if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
290
                            --$varL;
291
                        }
292
                        // also remove tabs after the object string name
293
                        $objStrName = substr($line, 0, $varL);
294
                        if ($objStrName !== '') {
295
                            $r = [];
296
                            if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
297
                                $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."');
298
                            } else {
299
                                $line = ltrim(substr($line, $varL));
300
                                if ($line === '') {
301
                                    $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
302
                                } else {
303
                                    // Checking for special TSparser properties (to change TS values at parsetime)
304
                                    $match = [];
305
                                    if ($line[0] === ':' && preg_match('/^:=\\s*([[:alpha:]]+)\\s*\\((.*)\\).*/', $line, $match)) {
306
                                        $tsFunc = $match[1];
307
                                        $tsFuncArg = $match[2];
308
                                        $val = $this->getVal($objStrName, $setup);
309
                                        $currentValue = $val[0] ?? null;
310
                                        $tsFuncArg = str_replace(['\\\\', '\\n', '\\t'], ['\\', LF, "\t"], $tsFuncArg);
311
                                        $newValue = $this->executeValueModifier($tsFunc, $tsFuncArg, $currentValue);
312
                                        if (isset($newValue)) {
313
                                            $line = '= ' . $newValue;
314
                                        }
315
                                    }
316
                                    switch ($line[0]) {
317
                                        case '=':
318
                                            if (strpos($objStrName, '.') !== false) {
319
                                                $value = [];
320
                                                $value[0] = trim(substr($line, 1));
321
                                                $this->setVal($objStrName, $setup, $value);
322
                                            } else {
323
                                                $setup[$objStrName] = trim(substr($line, 1));
324
                                                if ($this->lastComment && $this->regComments) {
325
                                                    // Setting comment..
326
                                                    $matchingCommentKey = $objStrName . '..';
327
                                                    if (isset($setup[$matchingCommentKey])) {
328
                                                        $setup[$matchingCommentKey] .= $this->lastComment;
329
                                                    } else {
330
                                                        $setup[$matchingCommentKey] = $this->lastComment;
331
                                                    }
332
                                                }
333
                                                if ($this->regLinenumbers) {
334
                                                    $setup[$objStrName . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
335
                                                }
336
                                            }
337
                                            break;
338
                                        case '{':
339
                                            $this->inBrace++;
340
                                            if (strpos($objStrName, '.') !== false) {
341
                                                $exitSig = $this->rollParseSub($objStrName, $setup);
342
                                                if ($exitSig) {
343
                                                    return $exitSig;
344
                                                }
345
                                            } else {
346
                                                if (!isset($setup[$objStrName . '.'])) {
347
                                                    $setup[$objStrName . '.'] = [];
348
                                                }
349
                                                $exitSig = $this->parseSub($setup[$objStrName . '.']);
350
                                                if ($exitSig) {
351
                                                    return $exitSig;
352
                                                }
353
                                            }
354
                                            break;
355
                                        case '(':
356
                                            $this->multiLineObject = $objStrName;
357
                                            $this->multiLineEnabled = true;
358
                                            $this->multiLineValue = [];
359
                                            break;
360
                                        case '<':
361
                                            $theVal = trim(substr($line, 1));
362
                                            if ($theVal[0] === '.') {
363
                                                $res = $this->getVal(substr($theVal, 1), $setup);
364
                                            } else {
365
                                                $res = $this->getVal($theVal, $this->setup);
366
                                            }
367
                                            // unserialize(serialize(...)) may look stupid but is needed because of some reference issues.
368
                                            // See forge issue #76919 and functional test hasFlakyReferences()
369
                                            $this->setVal($objStrName, $setup, unserialize(serialize($res), ['allowed_classes' => false]), true);
370
                                            break;
371
                                        case '>':
372
                                            $this->setVal($objStrName, $setup, 'UNSET');
373
                                            break;
374
                                        default:
375
                                            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
376
                                    }
377
                                }
378
                            }
379
                            $this->lastComment = '';
380
                        }
381
                    } elseif ($line[0] === '}') {
382
                        $this->inBrace--;
383
                        $this->lastComment = '';
384
                        if ($this->inBrace < 0) {
385
                            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': An end brace is in excess.', 1);
386
                            $this->inBrace = 0;
387
                        } else {
388
                            break;
389
                        }
390
                    } else {
391
                        // Comment. The comments are concatenated in this temporary string:
392
                        if ($this->regComments) {
393
                            $this->lastComment .= rtrim($line) . LF;
394
                        }
395
                    }
396
                    if (strpos($line, '### ERROR') === 0) {
397
                        $this->error(substr($line, 11));
398
                    }
399
                }
400
            }
401
            // Unset comment
402
            if ($this->commentSet) {
403
                if (strpos($line, '*/') !== false) {
404
                    $this->commentSet = false;
405
                }
406
            }
407
        }
408
        return '';
409
    }
410
411
    /**
412
     * Executes operator functions, called from TypoScript
413
     * example: page.10.value := appendString(!)
414
     *
415
     * @param string $modifierName TypoScript function called
416
     * @param string $modifierArgument Function arguments; In case of multiple arguments, the method must split on its own
417
     * @param string $currentValue Current TypoScript value
418
     * @return string|null Modified result or null for no modification
419
     */
420
    protected function executeValueModifier($modifierName, $modifierArgument = null, $currentValue = null)
421
    {
422
        $modifierArgumentAsString = (string)$modifierArgument;
423
        $currentValueAsString = (string)$currentValue;
424
        $newValue = null;
425
        switch ($modifierName) {
426
            case 'prependString':
427
                $newValue = $modifierArgumentAsString . $currentValueAsString;
428
                break;
429
            case 'appendString':
430
                $newValue = $currentValueAsString . $modifierArgumentAsString;
431
                break;
432
            case 'removeString':
433
                $newValue = str_replace($modifierArgumentAsString, '', $currentValueAsString);
434
                break;
435
            case 'replaceString':
436
                $modifierArgumentArray = explode('|', $modifierArgumentAsString, 2);
437
                $fromStr = $modifierArgumentArray[0] ?? '';
438
                $toStr = $modifierArgumentArray[1] ?? '';
439
                $newValue = str_replace($fromStr, $toStr, $currentValueAsString);
440
                break;
441
            case 'addToList':
442
                $newValue = ($currentValueAsString !== '' ? $currentValueAsString . ',' : '') . $modifierArgumentAsString;
443
                break;
444
            case 'removeFromList':
445
                $existingElements = GeneralUtility::trimExplode(',', $currentValueAsString);
446
                $removeElements = GeneralUtility::trimExplode(',', $modifierArgumentAsString);
447
                if (!empty($removeElements)) {
448
                    $newValue = implode(',', array_diff($existingElements, $removeElements));
449
                }
450
                break;
451
            case 'uniqueList':
452
                $elements = GeneralUtility::trimExplode(',', $currentValueAsString);
453
                $newValue = implode(',', array_unique($elements));
454
                break;
455
            case 'reverseList':
456
                $elements = GeneralUtility::trimExplode(',', $currentValueAsString);
457
                $newValue = implode(',', array_reverse($elements));
458
                break;
459
            case 'sortList':
460
                $elements = GeneralUtility::trimExplode(',', $currentValueAsString);
461
                $arguments = GeneralUtility::trimExplode(',', $modifierArgumentAsString);
462
                $arguments = array_map('strtolower', $arguments);
463
                $sort_flags = SORT_REGULAR;
464
                if (in_array('numeric', $arguments)) {
465
                    $sort_flags = SORT_NUMERIC;
466
                    // If the sorting modifier "numeric" is given, all values
467
                    // are checked and an exception is thrown if a non-numeric value is given
468
                    // otherwise there is a different behaviour between PHP7 and PHP 5.x
469
                    // See also the warning on http://us.php.net/manual/en/function.sort.php
470
                    foreach ($elements as $element) {
471
                        if (!is_numeric($element)) {
472
                            throw new \InvalidArgumentException('The list "' . $currentValueAsString . '" should be sorted numerically but contains a non-numeric value', 1438191758);
473
                        }
474
                    }
475
                }
476
                sort($elements, $sort_flags);
477
                if (in_array('descending', $arguments)) {
478
                    $elements = array_reverse($elements);
479
                }
480
                $newValue = implode(',', $elements);
481
                break;
482
            case 'getEnv':
483
                $environmentValue = getenv(trim($modifierArgumentAsString));
484
                if ($environmentValue !== false) {
485
                    $newValue = $environmentValue;
486
                }
487
                break;
488
            default:
489
                if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName])) {
490
                    $hookMethod = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName];
491
                    $params = ['currentValue' => $currentValue, 'functionArgument' => $modifierArgument];
492
                    $fakeThis = null;
493
                    $newValue = GeneralUtility::callUserFunction($hookMethod, $params, $fakeThis);
494
                } else {
495
                    self::getLogger()->warning('Missing function definition for ' . $modifierName . ' on TypoScript');
496
                }
497
        }
498
        return $newValue;
499
    }
500
501
    /**
502
     * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
503
     * thus having to recursively call itself to get the value
504
     *
505
     * @param string $string The object sub-path, eg "thisprop.another_prot
506
     * @param array $setup The local setup array from the function calling this function
507
     * @return string Returns the exitSignal
508
     * @see parseSub()
509
     */
510
    protected function rollParseSub($string, array &$setup)
511
    {
512
        if ((string)$string === '') {
513
            return '';
514
        }
515
516
        [$key, $remainingKey] = $this->parseNextKeySegment($string);
517
        $key .= '.';
518
        if (!isset($setup[$key])) {
519
            $setup[$key] = [];
520
        }
521
        $exitSig = $remainingKey === ''
522
            ? $this->parseSub($setup[$key])
523
            : $this->rollParseSub($remainingKey, $setup[$key]);
524
        return $exitSig ?: '';
525
    }
526
527
    /**
528
     * Get a value/property pair for an object path in TypoScript, eg. "myobject.myvalue.mysubproperty".
529
     * Here: Used by the "copy" operator, <
530
     *
531
     * @param string $string Object path for which to get the value
532
     * @param array $setup Global setup code if $string points to a global object path. But if string is prefixed with "." then its the local setup array.
533
     * @return array An array with keys 0/1 being value/property respectively
534
     */
535
    public function getVal($string, $setup)
536
    {
537
        if ((string)$string === '') {
538
            return [];
539
        }
540
541
        [$key, $remainingKey] = $this->parseNextKeySegment($string);
542
        $subKey = $key . '.';
543
        if ($remainingKey === '') {
544
            $retArr = [];
545
            if (isset($setup[$key])) {
546
                $retArr[0] = $setup[$key];
547
            }
548
            if (isset($setup[$subKey])) {
549
                $retArr[1] = $setup[$subKey];
550
            }
551
            return $retArr;
552
        }
553
        if ($setup[$subKey]) {
554
            return $this->getVal($remainingKey, $setup[$subKey]);
555
        }
556
557
        return [];
558
    }
559
560
    /**
561
     * Setting a value/property of an object string in the setup array.
562
     *
563
     * @param string $string The object sub-path, eg "thisprop.another_prot
564
     * @param array $setup The local setup array from the function calling this function.
565
     * @param array|string $value The value/property pair array to set. If only one of them is set, then the other is not touched (unless $wipeOut is set, which it is when copies are made which must include both value and property)
566
     * @param bool $wipeOut If set, then both value and property is wiped out when a copy is made of another value.
567
     */
568
    protected function setVal($string, array &$setup, $value, $wipeOut = false)
569
    {
570
        if ((string)$string === '') {
571
            return;
572
        }
573
574
        [$key, $remainingKey] = $this->parseNextKeySegment($string);
575
        $subKey = $key . '.';
576
        if ($remainingKey === '') {
577
            if ($value === 'UNSET') {
578
                unset($setup[$key]);
579
                unset($setup[$subKey]);
580
                if ($this->regLinenumbers) {
581
                    $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '>';
582
                }
583
            } else {
584
                $lnRegisDone = 0;
585
                if ($wipeOut) {
586
                    unset($setup[$key]);
587
                    unset($setup[$subKey]);
588
                    if ($this->regLinenumbers) {
589
                        $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '<';
590
                        $lnRegisDone = 1;
591
                    }
592
                }
593
                if (isset($value[0])) {
594
                    $setup[$key] = $value[0];
595
                }
596
                if (isset($value[1])) {
597
                    $setup[$subKey] = $value[1];
598
                }
599
                if ($this->lastComment && $this->regComments) {
600
                    $setup[$key . '..'] = $setup[$key . '..'] ?? '' . $this->lastComment;
601
                }
602
                if ($this->regLinenumbers && !$lnRegisDone) {
603
                    $setup[$key . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
604
                }
605
            }
606
        } else {
607
            if (!isset($setup[$subKey])) {
608
                $setup[$subKey] = [];
609
            }
610
            $this->setVal($remainingKey, $setup[$subKey], $value);
611
        }
612
    }
613
614
    /**
615
     * Determines the first key segment of a TypoScript key by searching for the first
616
     * unescaped dot in the given key string.
617
     *
618
     * Since the escape characters are only needed to correctly determine the key
619
     * segment any escape characters before the first unescaped dot are
620
     * stripped from the key.
621
     *
622
     * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
623
     * @return array Array with key segment and remaining part of $key
624
     */
625
    protected function parseNextKeySegment($key)
626
    {
627
        // if no dot is in the key, nothing to do
628
        $dotPosition = strpos($key, '.');
629
        if ($dotPosition === false) {
630
            return [$key, ''];
631
        }
632
633
        if (strpos($key, '\\') !== false) {
634
            // backslashes are in the key, so we do further parsing
635
636
            while ($dotPosition !== false) {
637
                if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($dotPosition > 0 && $ke...otPosition - 2] === '\', Probably Intended Meaning: $dotPosition > 0 && ($ke...tPosition - 2] === '\')
Loading history...
638
                    break;
639
                }
640
                // escaped dot found, continue
641
                $dotPosition = strpos($key, '.', $dotPosition + 1);
642
            }
643
644
            if ($dotPosition === false) {
645
                // no regular dot found
646
                $keySegment = $key;
647
                $remainingKey = '';
648
            } else {
649
                if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
650
                    $keySegment = substr($key, 0, $dotPosition - 1);
651
                } else {
652
                    $keySegment = substr($key, 0, $dotPosition);
653
                }
654
                $remainingKey = substr($key, $dotPosition + 1);
655
            }
656
657
            // fix key segment by removing escape sequences
658
            $keySegment = str_replace('\\.', '.', $keySegment);
659
        } else {
660
            // no backslash in the key, we're fine off
661
            [$keySegment, $remainingKey] = explode('.', $key, 2);
662
        }
663
        return [$keySegment, $remainingKey];
664
    }
665
666
    /**
667
     * Stacks errors/messages from the TypoScript parser into an internal array, $this->error
668
     * If "TT" is a global object (as it is in the frontend when backend users are logged in) the message will be registered here as well.
669
     *
670
     * @param string $err The error message string
671
     * @param int $num The error severity (in the scale of TimeTracker::setTSlogMessage: Approx: 2=warning, 1=info, 0=nothing, 3=fatal.)
672
     */
673
    protected function error($err, $num = 2)
674
    {
675
        $tt = $this->getTimeTracker();
676
        if ($tt !== null) {
677
            $tt->setTSlogMessage($err, $num);
678
        }
679
        $this->errors[] = [$err, $num, $this->rawP - 1, $this->lineNumberOffset];
680
    }
681
682
    /**
683
     * Checks the input string (un-parsed TypoScript) for include-commands ("<INCLUDE_TYPOSCRIPT: ....")
684
     * Use: \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines()
685
     *
686
     * @param string $string Unparsed TypoScript
687
     * @param int $cycle_counter Counter for detecting endless loops
688
     * @param bool $returnFiles When set an array containing the resulting typoscript and all included files will get returned
689
     * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
690
     * @return string|array Complete TypoScript with includes added.
691
     * @static
692
     */
693
    public static function checkIncludeLines($string, $cycle_counter = 1, $returnFiles = false, $parentFilenameOrPath = '')
694
    {
695
        $includedFiles = [];
696
        if ($cycle_counter > 100) {
697
            self::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
698
            if ($returnFiles) {
699
                return [
700
                    'typoscript' => '',
701
                    'files' => $includedFiles
702
                ];
703
            }
704
            return '
705
###
706
### ERROR: Recursion!
707
###
708
';
709
        }
710
711
        if ($string !== null) {
0 ignored issues
show
introduced by
The condition $string !== null is always true.
Loading history...
712
            $string = StringUtility::removeByteOrderMark($string);
713
        }
714
715
        // Checking for @import syntax imported files
716
        $string = self::addImportsFromExternalFiles($string, $cycle_counter, $returnFiles, $includedFiles, $parentFilenameOrPath);
717
718
        // If no tags found, no need to do slower preg_split
719
        if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== false) {
720
            $splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
721
            $parts = preg_split($splitRegEx, LF . $string . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
722
            $parts = is_array($parts) ? $parts : [];
0 ignored issues
show
introduced by
The condition is_array($parts) is always true.
Loading history...
723
724
            // First text part goes through
725
            $newString = ($parts[0] ?? '') . LF;
726
            $partCount = count($parts);
727
            for ($i = 1; $i + 3 < $partCount; $i += 4) {
728
                // $parts[$i] contains 'FILE' or 'DIR'
729
                // $parts[$i+1] contains relative file or directory path to be included
730
                // $parts[$i+2] optional properties of the INCLUDE statement
731
                // $parts[$i+3] next part of the typoscript string (part in between include-tags)
732
                $includeType = $parts[$i];
733
                $filename = $parts[$i + 1];
734
                $originalFilename = $filename;
735
                $optionalProperties = $parts[$i + 2];
736
                $tsContentsTillNextInclude = $parts[$i + 3];
737
738
                // Check condition
739
                $matches = preg_split('#(?i)condition\\s*=\\s*"((?:\\\\\\\\|\\\\"|[^\\"])*)"(\\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
740
                $matches = is_array($matches) ? $matches : [];
741
742
                // If there was a condition
743
                if (count($matches) > 1) {
744
                    // Unescape the condition
745
                    $condition = trim(stripslashes($matches[1]));
746
                    // If necessary put condition in square brackets
747
                    if ($condition[0] !== '[') {
748
                        $condition = '[' . $condition . ']';
749
                    }
750
751
                    /** @var AbstractConditionMatcher $conditionMatcher */
752
                    $conditionMatcher = null;
753
                    if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
754
                        && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
755
                    ) {
756
                        $conditionMatcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class);
757
                    } else {
758
                        $conditionMatcher = GeneralUtility::makeInstance(BackendConditionMatcher::class);
759
                    }
760
761
                    // If it didn't match then proceed to the next include, but prepend next normal (not file) part to output string
762
                    if (!$conditionMatcher->match($condition)) {
763
                        $newString .= $tsContentsTillNextInclude . LF;
764
                        continue;
765
                    }
766
                }
767
768
                // Resolve a possible relative paths if a parent file is given
769
                if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
770
                    $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
771
                }
772
773
                // There must be a line-break char after - not sure why this check is necessary, kept it for being 100% backwards compatible
774
                // An empty string is also ok (means that the next line is also a valid include_typoscript tag)
775
                if (!preg_match('/(^\\s*\\r?\\n|^$)/', $tsContentsTillNextInclude)) {
776
                    $newString .= self::typoscriptIncludeError('Invalid characters after <INCLUDE_TYPOSCRIPT: source="' . $includeType . ':' . $filename . '">-tag (rest of line must be empty).');
777
                } elseif (strpos('..', $filename) !== false) {
778
                    $newString .= self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
779
                } else {
780
                    switch (strtolower($includeType)) {
781
                        case 'file':
782
                            self::includeFile($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
783
                            break;
784
                        case 'dir':
785
                            self::includeDirectory($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
786
                            break;
787
                        default:
788
                            $newString .= self::typoscriptIncludeError('No valid option for INCLUDE_TYPOSCRIPT source property (valid options are FILE or DIR)');
789
                    }
790
                }
791
                // Prepend next normal (not file) part to output string
792
                $newString .= $tsContentsTillNextInclude . LF;
793
794
                // load default TypoScript for content rendering templates like
795
                // fluid_styled_content if those have been included through f.e.
796
                // <INCLUDE_TYPOSCRIPT: source="FILE:EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript">
797
                if (strpos(strtolower($filename), 'ext:') === 0) {
798
                    $filePointerPathParts = explode('/', substr($filename, 4));
799
800
                    // remove file part, determine whether to load setup or constants
801
                    [$includeType, ] = explode('.', (string)array_pop($filePointerPathParts));
802
803
                    if (in_array($includeType, ['setup', 'constants'])) {
804
                        // adapt extension key to required format (no underscores)
805
                        $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
806
807
                        // load default TypoScript
808
                        $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
809
                        if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
810
                            $newString .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
811
                        }
812
                    }
813
                }
814
            }
815
            // Add a line break before and after the included code in order to make sure that the parser always has a LF.
816
            $string = LF . trim($newString) . LF;
817
        }
818
        // When all included files should get returned, simply return a compound array containing
819
        // the TypoScript with all "includes" processed and the files which got included
820
        if ($returnFiles) {
821
            return [
822
                'typoscript' => $string,
823
                'files' => $includedFiles
824
            ];
825
        }
826
        return $string;
827
    }
828
829
    /**
830
     * Splits the unparsed TypoScript content into import statements
831
     *
832
     * @param string $typoScript unparsed TypoScript
833
     * @param int $cycleCounter counter to stop recursion
834
     * @param bool $returnFiles whether to populate the included Files or not
835
     * @param array $includedFiles - by reference - if any included files are added, they are added here
836
     * @param string $parentFilenameOrPath the current imported file to resolve relative paths - handled by reference
837
     * @return string the unparsed TypoScript with included external files
838
     */
839
    protected static function addImportsFromExternalFiles($typoScript, $cycleCounter, $returnFiles, &$includedFiles, &$parentFilenameOrPath)
840
    {
841
        // Check for new syntax "@import 'EXT:bennilove/Configuration/TypoScript/*'"
842
        if (strpos($typoScript, '@import \'') !== false || strpos($typoScript, '@import "') !== false) {
843
            $splitRegEx = '/\r?\n\s*@import\s[\'"]([^\'"]*)[\'"][\ \t]?/';
844
            $parts = preg_split($splitRegEx, LF . $typoScript . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
845
            $parts = is_array($parts) ? $parts : [];
0 ignored issues
show
introduced by
The condition is_array($parts) is always true.
Loading history...
846
            // First text part goes through
847
            $newString = $parts[0] . LF;
848
            $partCount = count($parts);
849
            for ($i = 1; $i + 2 <= $partCount; $i += 2) {
850
                $filename = $parts[$i];
851
                $tsContentsTillNextInclude = $parts[$i + 1];
852
                // Resolve a possible relative paths if a parent file is given
853
                if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
854
                    $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
855
                }
856
                $newString .= self::importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, $includedFiles);
857
                // Prepend next normal (not file) part to output string
858
                $newString .= $tsContentsTillNextInclude;
859
            }
860
            // Add a line break before and after the included code in order to make sure that the parser always has a LF.
861
            $typoScript = LF . trim($newString) . LF;
862
        }
863
        return $typoScript;
864
    }
865
866
    /**
867
     * Include file $filename. Contents of the file will be returned, filename is added to &$includedFiles.
868
     * Further include/import statements in the contents are processed recursively.
869
     *
870
     * @param string $filename Full absolute path+filename to the typoscript file to be included
871
     * @param int $cycleCounter Counter for detecting endless loops
872
     * @param bool $returnFiles When set, filenames of included files will be prepended to the array $includedFiles
873
     * @param array $includedFiles Array to which the filenames of included files will be prepended (referenced)
874
     * @return string the unparsed TypoScript content from external files
875
     */
876
    protected static function importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, array &$includedFiles)
877
    {
878
        if (strpos('..', $filename) !== false) {
879
            return self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
880
        }
881
882
        $content = '';
883
        $absoluteFileName = GeneralUtility::getFileAbsFileName($filename);
884
        if ((string)$absoluteFileName === '') {
885
            return self::typoscriptIncludeError('Illegal filepath "' . $filename . '".');
886
        }
887
888
        $finder = new Finder();
889
        $finder
890
            // no recursive mode on purpose
891
            ->depth(0)
892
            // no directories should be fetched
893
            ->files()
894
            ->sortByName();
895
896
        // Search all files in the folder
897
        if (is_dir($absoluteFileName)) {
898
            $finder
899
                ->in($absoluteFileName)
900
                ->name('*.typoscript');
901
            // Used for the TypoScript comments
902
            $readableFilePrefix = $filename;
903
        } else {
904
            try {
905
                // Apparently this is not a folder, so the restriction
906
                // is the folder so we restrict into this folder
907
                $finder->in(PathUtility::dirname($absoluteFileName));
908
                if (!is_file($absoluteFileName)
909
                    && strpos(PathUtility::basename($absoluteFileName), '*') === false
910
                    && substr(PathUtility::basename($absoluteFileName), -11) !== '.typoscript') {
911
                    $absoluteFileName .= '*.typoscript';
912
                }
913
                $finder->name(PathUtility::basename($absoluteFileName));
914
                $readableFilePrefix = PathUtility::dirname($filename);
915
            } catch (\InvalidArgumentException $e) {
916
                return self::typoscriptIncludeError($e->getMessage());
917
            }
918
        }
919
920
        foreach ($finder as $fileObject) {
921
            // Clean filename output for comments
922
            $readableFileName = rtrim($readableFilePrefix, '/') . '/' . $fileObject->getFilename();
923
            $content .= LF . '### @import \'' . $readableFileName . '\' begin ###' . LF;
924
            // Check for allowed files
925
            if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($fileObject->getFilename())) {
926
                $content .= self::typoscriptIncludeError('File "' . $readableFileName . '" was not included since it is not allowed due to fileDenyPattern.');
927
            } else {
928
                $includedFiles[] = $fileObject->getPathname();
929
                // check for includes in included text
930
                $included_text = self::checkIncludeLines($fileObject->getContents(), $cycleCounter++, $returnFiles, $absoluteFileName);
931
                // If the method also has to return all included files, merge currently included
932
                // files with files included by recursively calling itself
933
                if ($returnFiles && is_array($included_text)) {
934
                    $includedFiles = array_merge($includedFiles, $included_text['files']);
935
                    $included_text = $included_text['typoscript'];
936
                }
937
                $content .= $included_text . LF;
938
            }
939
            $content .= '### @import \'' . $readableFileName . '\' end ###' . LF . LF;
940
941
            // load default TypoScript for content rendering templates like
942
            // fluid_styled_content if those have been included through e.g.
943
            // @import "fluid_styled_content/Configuration/TypoScript/setup.typoscript"
944
            if (strpos(strtoupper($filename), 'EXT:') === 0) {
945
                $filePointerPathParts = explode('/', substr($filename, 4));
946
                // remove file part, determine whether to load setup or constants
947
                [$includeType] = explode('.', (string)array_pop($filePointerPathParts));
948
949
                if (in_array($includeType, ['setup', 'constants'], true)) {
950
                    // adapt extension key to required format (no underscores)
951
                    $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
952
953
                    // load default TypoScript
954
                    $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
955
                    if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
956
                        $content .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
957
                    }
958
                }
959
            }
960
        }
961
962
        if (empty($content)) {
963
            return self::typoscriptIncludeError('No file or folder found for importing TypoScript on "' . $filename . '".');
964
        }
965
        return $content;
966
    }
967
968
    /**
969
     * Include file $filename. Contents of the file will be prepended to &$newstring, filename to &$includedFiles
970
     * Further include_typoscript tags in the contents are processed recursively
971
     *
972
     * @param string $filename Relative path to the typoscript file to be included
973
     * @param int $cycle_counter Counter for detecting endless loops
974
     * @param bool $returnFiles When set, filenames of included files will be prepended to the array $includedFiles
975
     * @param string $newString The output string to which the content of the file will be prepended (referenced
976
     * @param array $includedFiles Array to which the filenames of included files will be prepended (referenced)
977
     * @param string $optionalProperties
978
     * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
979
     * @static
980
     * @internal
981
     */
982
    public static function includeFile($filename, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
983
    {
984
        // Resolve a possible relative paths if a parent file is given
985
        if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
986
            $absfilename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
987
        } else {
988
            $absfilename = $filename;
989
        }
990
        $absfilename = GeneralUtility::getFileAbsFileName($absfilename);
991
992
        $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> BEGIN:' . LF;
993
        if ((string)$filename !== '') {
994
            // Must exist and must not contain '..' and must be relative
995
            // Check for allowed files
996
            if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($absfilename)) {
997
                $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
998
            } else {
999
                $fileExists = false;
1000
                if (@file_exists($absfilename)) {
1001
                    $fileExists = true;
1002
                }
1003
1004
                if ($fileExists) {
1005
                    $includedFiles[] = $absfilename;
1006
                    // check for includes in included text
1007
                    $included_text = self::checkIncludeLines((string)file_get_contents($absfilename), $cycle_counter + 1, $returnFiles, $absfilename);
1008
                    // If the method also has to return all included files, merge currently included
1009
                    // files with files included by recursively calling itself
1010
                    if ($returnFiles && is_array($included_text)) {
1011
                        $includedFiles = array_merge($includedFiles, $included_text['files']);
1012
                        $included_text = $included_text['typoscript'];
1013
                    }
1014
                    $newString .= $included_text . LF;
1015
                } else {
1016
                    $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not found.');
1017
                }
1018
            }
1019
        }
1020
        $newString .= '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> END:' . LF . LF;
1021
    }
1022
1023
    /**
1024
     * Include all files with matching Typoscript extensions in directory $dirPath. Contents of the files are
1025
     * prepended to &$newstring, filename to &$includedFiles.
1026
     * Order of the directory items to be processed: files first, then directories, both in alphabetical order.
1027
     * Further include_typoscript tags in the contents of the files are processed recursively.
1028
     *
1029
     * @param string $dirPath Relative path to the directory to be included
1030
     * @param int $cycle_counter Counter for detecting endless loops
1031
     * @param bool $returnFiles When set, filenames of included files will be prepended to the array $includedFiles
1032
     * @param string $newString The output string to which the content of the file will be prepended (referenced)
1033
     * @param array $includedFiles Array to which the filenames of included files will be prepended (referenced)
1034
     * @param string $optionalProperties
1035
     * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1036
     * @static
1037
     */
1038
    protected static function includeDirectory($dirPath, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1039
    {
1040
        // Extract the value of the property extensions="..."
1041
        $matches = preg_split('#(?i)extensions\s*=\s*"([^"]*)"(\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
1042
        $matches = is_array($matches) ? $matches : [];
0 ignored issues
show
introduced by
The condition is_array($matches) is always true.
Loading history...
1043
        if (count($matches) > 1) {
1044
            $includedFileExtensions = $matches[1];
1045
        } else {
1046
            $includedFileExtensions = '';
1047
        }
1048
1049
        // Resolve a possible relative paths if a parent file is given
1050
        if ($parentFilenameOrPath !== '' && $dirPath[0] === '.') {
1051
            $resolvedDirPath = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $dirPath);
1052
        } else {
1053
            $resolvedDirPath = $dirPath;
1054
        }
1055
        $absDirPath = GeneralUtility::getFileAbsFileName($resolvedDirPath);
1056
        if ($absDirPath) {
1057
            $absDirPath = rtrim($absDirPath, '/') . '/';
1058
            $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> BEGIN:' . LF;
1059
            // Get alphabetically sorted file index in array
1060
            $fileIndex = GeneralUtility::getAllFilesAndFoldersInPath([], $absDirPath, $includedFileExtensions);
1061
            // Prepend file contents to $newString
1062
            $prefixLength = strlen(Environment::getPublicPath() . '/');
1063
            foreach ($fileIndex as $absFileRef) {
1064
                $relFileRef = substr($absFileRef, $prefixLength);
1065
                self::includeFile($relFileRef, $cycle_counter, $returnFiles, $newString, $includedFiles, '', $absDirPath);
1066
            }
1067
            $newString .= '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> END:' . LF . LF;
1068
        } else {
1069
            $newString .= self::typoscriptIncludeError('The path "' . $resolvedDirPath . '" is invalid.');
1070
        }
1071
    }
1072
1073
    /**
1074
     * Process errors in INCLUDE_TYPOSCRIPT tags
1075
     * Errors are logged and printed in the concatenated TypoScript result (as can be seen in Template Analyzer)
1076
     *
1077
     * @param string $error Text of the error message
1078
     * @return string The error message encapsulated in comments
1079
     * @static
1080
     */
1081
    protected static function typoscriptIncludeError($error)
1082
    {
1083
        self::getLogger()->warning($error);
1084
        return "\n###\n### ERROR: " . $error . "\n###\n\n";
1085
    }
1086
1087
    /**
1088
     * Parses the string in each value of the input array for include-commands
1089
     *
1090
     * @param array $array Array with TypoScript in each value
1091
     * @return array Same array but where the values has been parsed for include-commands
1092
     */
1093
    public static function checkIncludeLines_array(array $array)
1094
    {
1095
        foreach ($array as $k => $v) {
1096
            $array[$k] = self::checkIncludeLines($array[$k]);
1097
        }
1098
        return $array;
1099
    }
1100
1101
    /**
1102
     * Search for commented INCLUDE_TYPOSCRIPT statements
1103
     * and save the content between the BEGIN and the END line to the specified file
1104
     *
1105
     * @param string  $string Template content
1106
     * @param int $cycle_counter Counter for detecting endless loops
1107
     * @param array   $extractedFileNames
1108
     * @param string  $parentFilenameOrPath
1109
     *
1110
     * @throws \RuntimeException
1111
     * @throws \UnexpectedValueException
1112
     * @return string Template content with uncommented include statements
1113
     * @internal
1114
     */
1115
    public static function extractIncludes($string, $cycle_counter = 1, array $extractedFileNames = [], $parentFilenameOrPath = '')
1116
    {
1117
        if ($cycle_counter > 10) {
1118
            self::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
1119
            return '
1120
###
1121
### ERROR: Recursion!
1122
###
1123
';
1124
        }
1125
        $expectedEndTag = '';
1126
        $fileContent = [];
1127
        $restContent = [];
1128
        $fileName = null;
1129
        $inIncludePart = false;
1130
        $lines = preg_split("/\r\n|\n|\r/", $string);
1131
        $skipNextLineIfEmpty = false;
1132
        $openingCommentedIncludeStatement = null;
1133
        $optionalProperties = '';
1134
        foreach ($lines as $line) {
1135
            // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1136
            // an additional empty line, remove this again
1137
            if ($skipNextLineIfEmpty) {
1138
                if (trim($line) === '') {
1139
                    continue;
1140
                }
1141
                $skipNextLineIfEmpty = false;
1142
            }
1143
1144
            // Outside commented include statements
1145
            if (!$inIncludePart) {
1146
                // Search for beginning commented include statements
1147
                if (preg_match('/###\\s*<INCLUDE_TYPOSCRIPT:\\s*source\\s*=\\s*"\\s*((?i)file|dir)\\s*:\\s*([^"]*)"(.*)>\\s*BEGIN/i', $line, $matches)) {
1148
                    // Found a commented include statement
1149
1150
                    // Save this line in case there is no ending tag
1151
                    $openingCommentedIncludeStatement = trim($line);
1152
                    $openingCommentedIncludeStatement = preg_replace('/\\s*### Warning: .*###\\s*/', '', $openingCommentedIncludeStatement);
1153
1154
                    // type of match: FILE or DIR
1155
                    $inIncludePart = strtoupper($matches[1]);
1156
                    $fileName = $matches[2];
1157
                    $optionalProperties = $matches[3];
1158
1159
                    $expectedEndTag = '### <INCLUDE_TYPOSCRIPT: source="' . $inIncludePart . ':' . $fileName . '"' . $optionalProperties . '> END';
1160
                    // Strip all whitespace characters to make comparison safer
1161
                    $expectedEndTag = strtolower(preg_replace('/\s/', '', $expectedEndTag) ?? '');
1162
                } else {
1163
                    // If this is not a beginning commented include statement this line goes into the rest content
1164
                    $restContent[] = $line;
1165
                }
1166
            } else {
1167
                // Inside commented include statements
1168
                // Search for the matching ending commented include statement
1169
                $strippedLine = preg_replace('/\s/', '', $line);
1170
                if (stripos($strippedLine, $expectedEndTag) !== false) {
1171
                    // Found the matching ending include statement
1172
                    $fileContentString = implode(PHP_EOL, $fileContent);
1173
1174
                    // Write the content to the file
1175
1176
                    // Resolve a possible relative paths if a parent file is given
1177
                    if ($parentFilenameOrPath !== '' && $fileName[0] === '.') {
1178
                        $realFileName = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $fileName);
1179
                    } else {
1180
                        $realFileName = $fileName;
1181
                    }
1182
                    $realFileName = GeneralUtility::getFileAbsFileName($realFileName);
1183
1184
                    if ($inIncludePart === 'FILE') {
1185
                        // Some file checks
1186
                        if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($realFileName)) {
1187
                            throw new \UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
1188
                        }
1189
                        if (empty($realFileName)) {
1190
                            throw new \UnexpectedValueException(sprintf('"%s" is not a valid file location.', $fileName), 1294586441);
1191
                        }
1192
                        if (!is_writable($realFileName)) {
1193
                            throw new \RuntimeException(sprintf('"%s" is not writable.', $fileName), 1294586442);
1194
                        }
1195
                        if (in_array($realFileName, $extractedFileNames)) {
1196
                            throw new \RuntimeException(sprintf('Recursive/multiple inclusion of file "%s"', $realFileName), 1294586443);
1197
                        }
1198
                        $extractedFileNames[] = $realFileName;
1199
1200
                        // Recursive call to detected nested commented include statements
1201
                        $fileContentString = self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1202
1203
                        // Write the content to the file
1204
                        if (!GeneralUtility::writeFile($realFileName, $fileContentString)) {
1205
                            throw new \RuntimeException(sprintf('Could not write file "%s"', $realFileName), 1294586444);
1206
                        }
1207
                        // Insert reference to the file in the rest content
1208
                        $restContent[] = '<INCLUDE_TYPOSCRIPT: source="FILE:' . $fileName . '"' . $optionalProperties . '>';
1209
                    } else {
1210
                        // must be DIR
1211
1212
                        // Some file checks
1213
                        if (empty($realFileName)) {
1214
                            throw new \UnexpectedValueException(sprintf('"%s" is not a valid location.', $fileName), 1366493602);
1215
                        }
1216
                        if (!is_dir($realFileName)) {
1217
                            throw new \RuntimeException(sprintf('"%s" is not a directory.', $fileName), 1366493603);
1218
                        }
1219
                        if (in_array($realFileName, $extractedFileNames)) {
1220
                            throw new \RuntimeException(sprintf('Recursive/multiple inclusion of directory "%s"', $realFileName), 1366493604);
1221
                        }
1222
                        $extractedFileNames[] = $realFileName;
1223
1224
                        // Recursive call to detected nested commented include statements
1225
                        self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1226
1227
                        // just drop content between tags since it should usually just contain individual files from that dir
1228
1229
                        // Insert reference to the dir in the rest content
1230
                        $restContent[] = '<INCLUDE_TYPOSCRIPT: source="DIR:' . $fileName . '"' . $optionalProperties . '>';
1231
                    }
1232
1233
                    // Reset variables (preparing for the next commented include statement)
1234
                    $fileContent = [];
1235
                    $fileName = null;
1236
                    $inIncludePart = false;
1237
                    $openingCommentedIncludeStatement = null;
1238
                    // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1239
                    // an additional empty line, remove this again
1240
                    $skipNextLineIfEmpty = true;
1241
                } else {
1242
                    // If this is not an ending commented include statement this line goes into the file content
1243
                    $fileContent[] = $line;
1244
                }
1245
            }
1246
        }
1247
        // If we're still inside commented include statements copy the lines back to the rest content
1248
        if ($inIncludePart) {
1249
            $restContent[] = $openingCommentedIncludeStatement . ' ### Warning: Corresponding end line missing! ###';
1250
            $restContent = array_merge($restContent, $fileContent);
1251
        }
1252
        $restContentString = implode(PHP_EOL, $restContent);
1253
        return $restContentString;
1254
    }
1255
1256
    /**
1257
     * Processes the string in each value of the input array with extractIncludes
1258
     *
1259
     * @param array $array Array with TypoScript in each value
1260
     * @return array Same array but where the values has been processed with extractIncludes
1261
     */
1262
    public static function extractIncludes_array(array $array)
1263
    {
1264
        foreach ($array as $k => $v) {
1265
            $array[$k] = self::extractIncludes($array[$k]);
1266
        }
1267
        return $array;
1268
    }
1269
1270
    /**
1271
     * @param string $string
1272
     * @return string
1273
     * @deprecated since v11, will be removed in v12.
1274
     */
1275
    public function doSyntaxHighlight($string)
1276
    {
1277
        return $string;
1278
    }
1279
1280
    /**
1281
     * @return TimeTracker
1282
     */
1283
    protected function getTimeTracker()
1284
    {
1285
        return GeneralUtility::makeInstance(TimeTracker::class);
1286
    }
1287
1288
    /**
1289
     * Get a logger instance
1290
     *
1291
     * This class uses logging mostly in static functions, hence we need a static getter for the logger.
1292
     * Injection of a logger instance via GeneralUtility::makeInstance is not possible.
1293
     *
1294
     * @return LoggerInterface
1295
     */
1296
    protected static function getLogger()
1297
    {
1298
        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
1299
    }
1300
}
1301