Completed
Push — master ( 81ff7f...18e02f )
by
unknown
26:06 queued 11:27
created

TypoScriptParser::setVal()   C

Complexity

Conditions 14
Paths 53

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 30
nc 53
nop 4
dl 0
loc 43
rs 6.2666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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