Passed
Push — master ( 4dfe0c...5e249b )
by
unknown
13:51 queued 01:38
created

TypoScriptParser::includeFile()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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