Passed
Branch master (6c65a4)
by Christian
27:15 queued 11:09
created

TypoScriptParser::getLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Core\TypoScript\Parser;
3
4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use Psr\Log\LoggerInterface;
18
use Symfony\Component\Finder\Finder;
19
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as BackendConditionMatcher;
20
use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
21
use TYPO3\CMS\Core\Log\LogManager;
22
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
23
use TYPO3\CMS\Core\TypoScript\ExtendedTemplateService;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Core\Utility\MathUtility;
26
use TYPO3\CMS\Core\Utility\PathUtility;
27
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher;
28
29
/**
30
 * The TypoScript parser
31
 */
32
class TypoScriptParser
33
{
34
    /**
35
     * TypoScript hierarchy being build during parsing.
36
     *
37
     * @var array
38
     */
39
    public $setup = [];
40
41
    /**
42
     * Raw data, the input string exploded by LF
43
     *
44
     * @var array
45
     */
46
    public $raw;
47
48
    /**
49
     * Pointer to entry in raw data array
50
     *
51
     * @var int
52
     */
53
    public $rawP;
54
55
    /**
56
     * Holding the value of the last comment
57
     *
58
     * @var string
59
     */
60
    public $lastComment = '';
61
62
    /**
63
     * Internally set, used as internal flag to create a multi-line comment (one of those like /* ... * /
64
     *
65
     * @var bool
66
     */
67
    public $commentSet = false;
68
69
    /**
70
     * Internally set, when multiline value is accumulated
71
     *
72
     * @var bool
73
     */
74
    public $multiLineEnabled = false;
75
76
    /**
77
     * Internally set, when multiline value is accumulated
78
     *
79
     * @var string
80
     */
81
    public $multiLineObject = '';
82
83
    /**
84
     * Internally set, when multiline value is accumulated
85
     *
86
     * @var array
87
     */
88
    public $multiLineValue = [];
89
90
    /**
91
     * Internally set, when in brace. Counter.
92
     *
93
     * @var int
94
     */
95
    public $inBrace = 0;
96
97
    /**
98
     * For each condition this flag is set, if the condition is TRUE,
99
     * else it's cleared. Then it's used by the [ELSE] condition to determine if the next part should be parsed.
100
     *
101
     * @var bool
102
     */
103
    public $lastConditionTrue = true;
104
105
    /**
106
     * Tracking all conditions found
107
     *
108
     * @var array
109
     */
110
    public $sections = [];
111
112
    /**
113
     * Tracking all matching conditions found
114
     *
115
     * @var array
116
     */
117
    public $sectionsMatch = [];
118
119
    /**
120
     * If set, then syntax highlight mode is on; Call the function syntaxHighlight() to use this function
121
     *
122
     * @var bool
123
     */
124
    public $syntaxHighLight = false;
125
126
    /**
127
     * Syntax highlight data is accumulated in this array. Used by syntaxHighlight_print() to construct the output.
128
     *
129
     * @var array
130
     */
131
    public $highLightData = [];
132
133
    /**
134
     * Syntax highlight data keeping track of the curly brace level for each line
135
     *
136
     * @var array
137
     */
138
    public $highLightData_bracelevel = [];
139
140
    /**
141
     * DO NOT register the comments. This is default for the ordinary sitetemplate!
142
     *
143
     * @var bool
144
     */
145
    public $regComments = false;
146
147
    /**
148
     * DO NOT register the linenumbers. This is default for the ordinary sitetemplate!
149
     *
150
     * @var bool
151
     */
152
    public $regLinenumbers = false;
153
154
    /**
155
     * Error accumulation array.
156
     *
157
     * @var array
158
     */
159
    public $errors = [];
160
161
    /**
162
     * Used for the error messages line number reporting. Set externally.
163
     *
164
     * @var int
165
     */
166
    public $lineNumberOffset = 0;
167
168
    /**
169
     * Line for break point.
170
     *
171
     * @var int
172
     */
173
    public $breakPointLN = 0;
174
175
    /**
176
     * @var array
177
     */
178
    public $highLightStyles = [
179
        'prespace' => ['<span class="ts-prespace">', '</span>'],
180
        // Space before any content on a line
181
        'objstr_postspace' => ['<span class="ts-objstr_postspace">', '</span>'],
182
        // Space after the object string on a line
183
        'operator_postspace' => ['<span class="ts-operator_postspace">', '</span>'],
184
        // Space after the operator on a line
185
        'operator' => ['<span class="ts-operator">', '</span>'],
186
        // The operator char
187
        'value' => ['<span class="ts-value">', '</span>'],
188
        // The value of a line
189
        'objstr' => ['<span class="ts-objstr">', '</span>'],
190
        // The object string of a line
191
        'value_copy' => ['<span class="ts-value_copy">', '</span>'],
192
        // The value when the copy syntax (<) is used; that means the object reference
193
        'value_unset' => ['<span class="ts-value_unset">', '</span>'],
194
        // The value when an object is unset. Should not exist.
195
        'ignored' => ['<span class="ts-ignored">', '</span>'],
196
        // The "rest" of a line which will be ignored.
197
        'default' => ['<span class="ts-default">', '</span>'],
198
        // The default style if none other is applied.
199
        'comment' => ['<span class="ts-comment">', '</span>'],
200
        // Comment lines
201
        'condition' => ['<span class="ts-condition">', '</span>'],
202
        // Conditions
203
        'error' => ['<span class="ts-error">', '</span>'],
204
        // Error messages
205
        'linenum' => ['<span class="ts-linenum">', '</span>']
206
    ];
207
208
    /**
209
     * Additional attributes for the <span> tags for a blockmode line
210
     *
211
     * @var string
212
     */
213
    public $highLightBlockStyles = '';
214
215
    /**
216
     * The hex-HTML color for the blockmode
217
     *
218
     * @var string
219
     */
220
    public $highLightBlockStyles_basecolor = '#cccccc';
221
222
    /**
223
     * @var \TYPO3\CMS\Core\TypoScript\ExtendedTemplateService
224
     */
225
    public $parentObject;
226
227
    /**
228
     * Start parsing the input TypoScript text piece. The result is stored in $this->setup
229
     *
230
     * @param string $string The TypoScript text
231
     * @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])
232
     */
233
    public function parse($string, $matchObj = '')
234
    {
235
        $this->raw = explode(LF, $string);
236
        $this->rawP = 0;
237
        $pre = '[GLOBAL]';
238
        while ($pre) {
239
            if ($this->breakPointLN && $pre === '[_BREAK]') {
240
                $this->error('Breakpoint at ' . ($this->lineNumberOffset + $this->rawP - 2) . ': Line content was "' . $this->raw[$this->rawP - 2] . '"', 1);
241
                break;
242
            }
243
            $preUppercase = strtoupper($pre);
244
            if ($pre[0] === '[' &&
245
                ($preUppercase === '[GLOBAL]' ||
246
                    $preUppercase === '[END]' ||
247
                    !$this->lastConditionTrue && $preUppercase === '[ELSE]')
248
            ) {
249
                $pre = trim($this->parseSub($this->setup));
250
                $this->lastConditionTrue = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $lastConditionTrue was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
251
            } else {
252
                // We're in a specific section. Therefore we log this section
253
                $specificSection = $preUppercase !== '[ELSE]';
254
                if ($specificSection) {
255
                    $this->sections[md5($pre)] = $pre;
256
                }
257
                if (is_object($matchObj) && $matchObj->match($pre) || $this->syntaxHighLight) {
258
                    if ($specificSection) {
259
                        $this->sectionsMatch[md5($pre)] = $pre;
260
                    }
261
                    $pre = trim($this->parseSub($this->setup));
262
                    $this->lastConditionTrue = 1;
263
                } else {
264
                    $pre = $this->nextDivider();
265
                    $this->lastConditionTrue = 0;
266
                }
267
            }
268
        }
269
        if ($this->inBrace) {
270
            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)', 1);
271
        }
272
        if ($this->multiLineEnabled) {
273
            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': A multiline value section is not ended with a parenthesis!', 1);
274
        }
275
        $this->lineNumberOffset += count($this->raw) + 1;
276
    }
277
278
    /**
279
     * 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.
280
     *
281
     * @return string The condition value
282
     * @see parse()
283
     */
284
    public function nextDivider()
285
    {
286
        while (isset($this->raw[$this->rawP])) {
287
            $line = trim($this->raw[$this->rawP]);
288
            $this->rawP++;
289
            if ($line && $line[0] === '[') {
290
                return $line;
291
            }
292
        }
293
        return '';
294
    }
295
296
    /**
297
     * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
298
     *
299
     * @param array $setup Reference to the setup array in which to accumulate the values.
300
     * @return string|null Returns the string of the condition found, the exit signal or possible nothing (if it completed parsing with no interruptions)
301
     */
302
    public function parseSub(array &$setup)
303
    {
304
        while (isset($this->raw[$this->rawP])) {
305
            $line = ltrim($this->raw[$this->rawP]);
306
            $lineP = $this->rawP;
307
            $this->rawP++;
308
            if ($this->syntaxHighLight) {
309
                $this->regHighLight('prespace', $lineP, strlen($line));
310
            }
311
            // Breakpoint?
312
            // By adding 1 we get that line processed
313
            if ($this->breakPointLN && $this->lineNumberOffset + $this->rawP - 1 === $this->breakPointLN + 1) {
314
                return '[_BREAK]';
315
            }
316
            // Set comment flag?
317
            if (!$this->multiLineEnabled && strpos($line, '/*') === 0) {
318
                $this->commentSet = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $commentSet was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
319
            }
320
            // If $this->multiLineEnabled we will go and get the line values here because we know, the first if() will be TRUE.
321
            if (!$this->commentSet && ($line || $this->multiLineEnabled)) {
322
                // If multiline is enabled. Escape by ')'
323
                if ($this->multiLineEnabled) {
324
                    // Multiline ends...
325
                    if ($line[0] === ')') {
326
                        if ($this->syntaxHighLight) {
327
                            $this->regHighLight('operator', $lineP, strlen($line) - 1);
328
                        }
329
                        // Disable multiline
330
                        $this->multiLineEnabled = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $multiLineEnabled was declared of type boolean, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
331
                        $theValue = implode($this->multiLineValue, LF);
0 ignored issues
show
Bug introduced by
$this->multiLineValue of type array is incompatible with the type string expected by parameter $glue of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

331
                        $theValue = implode(/** @scrutinizer ignore-type */ $this->multiLineValue, LF);
Loading history...
332
                        if (strpos($this->multiLineObject, '.') !== false) {
333
                            // Set the value deeper.
334
                            $this->setVal($this->multiLineObject, $setup, [$theValue]);
335
                        } else {
336
                            // Set value regularly
337
                            $setup[$this->multiLineObject] = $theValue;
338
                            if ($this->lastComment && $this->regComments) {
339
                                $setup[$this->multiLineObject . '..'] .= $this->lastComment;
340
                            }
341
                            if ($this->regLinenumbers) {
342
                                $setup[$this->multiLineObject . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
343
                            }
344
                        }
345
                    } else {
346
                        if ($this->syntaxHighLight) {
347
                            $this->regHighLight('value', $lineP);
348
                        }
349
                        $this->multiLineValue[] = $this->raw[$this->rawP - 1];
350
                    }
351
                } elseif ($this->inBrace === 0 && $line[0] === '[') {
352
                    // Beginning of condition (only on level zero compared to brace-levels
353
                    if ($this->syntaxHighLight) {
354
                        $this->regHighLight('condition', $lineP);
355
                    }
356
                    return $line;
357
                } else {
358
                    // Return if GLOBAL condition is set - no matter what.
359
                    if ($line[0] === '[' && stripos($line, '[GLOBAL]') !== false) {
360
                        if ($this->syntaxHighLight) {
361
                            $this->regHighLight('condition', $lineP);
362
                        }
363
                        $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': On return to [GLOBAL] scope, the script was short of ' . $this->inBrace . ' end brace(s)', 1);
364
                        $this->inBrace = 0;
365
                        return $line;
366
                    }
367
                    if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
368
                        // If not brace-end or comment
369
                        // Find object name string until we meet an operator
370
                        $varL = strcspn($line, TAB . ' {=<>(');
371
                        // check for special ":=" operator
372
                        if ($varL > 0 && substr($line, $varL-1, 2) === ':=') {
373
                            --$varL;
374
                        }
375
                        // also remove tabs after the object string name
376
                        $objStrName = substr($line, 0, $varL);
377
                        if ($this->syntaxHighLight) {
378
                            $this->regHighLight('objstr', $lineP, strlen(substr($line, $varL)));
379
                        }
380
                        if ($objStrName !== '') {
381
                            $r = [];
382
                            if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
383
                                $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."');
384
                            } else {
385
                                $line = ltrim(substr($line, $varL));
386
                                if ($this->syntaxHighLight) {
387
                                    $this->regHighLight('objstr_postspace', $lineP, strlen($line));
388
                                    if ($line !== '') {
389
                                        $this->regHighLight('operator', $lineP, strlen($line) - 1);
390
                                        $this->regHighLight('operator_postspace', $lineP, strlen(ltrim(substr($line, 1))));
391
                                    }
392
                                }
393
                                if ($line === '') {
394
                                    $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
395
                                } else {
396
                                    // Checking for special TSparser properties (to change TS values at parsetime)
397
                                    $match = [];
398
                                    if ($line[0] === ':' && preg_match('/^:=\\s*([[:alpha:]]+)\\s*\\((.*)\\).*/', $line, $match)) {
399
                                        $tsFunc = $match[1];
400
                                        $tsFuncArg = $match[2];
401
                                        list($currentValue) = $this->getVal($objStrName, $setup);
402
                                        $tsFuncArg = str_replace(['\\\\', '\\n', '\\t'], ['\\', LF, TAB], $tsFuncArg);
403
                                        $newValue = $this->executeValueModifier($tsFunc, $tsFuncArg, $currentValue);
404
                                        if (isset($newValue)) {
405
                                            $line = '= ' . $newValue;
406
                                        }
407
                                    }
408
                                    switch ($line[0]) {
409
                                        case '=':
410
                                            if ($this->syntaxHighLight) {
411
                                                $this->regHighLight('value', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
412
                                            }
413
                                            if (strpos($objStrName, '.') !== false) {
414
                                                $value = [];
415
                                                $value[0] = trim(substr($line, 1));
416
                                                $this->setVal($objStrName, $setup, $value);
417
                                            } else {
418
                                                $setup[$objStrName] = trim(substr($line, 1));
419
                                                if ($this->lastComment && $this->regComments) {
420
                                                    // Setting comment..
421
                                                    $setup[$objStrName . '..'] .= $this->lastComment;
422
                                                }
423
                                                if ($this->regLinenumbers) {
424
                                                    $setup[$objStrName . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
425
                                                }
426
                                            }
427
                                            break;
428
                                        case '{':
429
                                            $this->inBrace++;
430
                                            if (strpos($objStrName, '.') !== false) {
431
                                                $exitSig = $this->rollParseSub($objStrName, $setup);
432
                                                if ($exitSig) {
433
                                                    return $exitSig;
434
                                                }
435
                                            } else {
436
                                                if (!isset($setup[$objStrName . '.'])) {
437
                                                    $setup[$objStrName . '.'] = [];
438
                                                }
439
                                                $exitSig = $this->parseSub($setup[$objStrName . '.']);
440
                                                if ($exitSig) {
441
                                                    return $exitSig;
442
                                                }
443
                                            }
444
                                            break;
445
                                        case '(':
446
                                            $this->multiLineObject = $objStrName;
447
                                            $this->multiLineEnabled = 1;
448
                                            $this->multiLineValue = [];
449
                                            break;
450
                                        case '<':
451
                                            if ($this->syntaxHighLight) {
452
                                                $this->regHighLight('value_copy', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
453
                                            }
454
                                            $theVal = trim(substr($line, 1));
455
                                            if ($theVal[0] === '.') {
456
                                                $res = $this->getVal(substr($theVal, 1), $setup);
457
                                            } else {
458
                                                $res = $this->getVal($theVal, $this->setup);
459
                                            }
460
                                            // unserialize(serialize(...)) may look stupid but is needed because of some reference issues.
461
                                            // See forge issue #76919 and functional test hasFlakyReferences()
462
                                            $this->setVal($objStrName, $setup, unserialize(serialize($res)), 1);
463
                                            break;
464
                                        case '>':
465
                                            if ($this->syntaxHighLight) {
466
                                                $this->regHighLight('value_unset', $lineP, strlen(ltrim(substr($line, 1))) - strlen(trim(substr($line, 1))));
467
                                            }
468
                                            $this->setVal($objStrName, $setup, 'UNSET');
469
                                            break;
470
                                        default:
471
                                            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({');
472
                                    }
473
                                }
474
                            }
475
                            $this->lastComment = '';
476
                        }
477
                    } elseif ($line[0] === '}') {
478
                        $this->inBrace--;
479
                        $this->lastComment = '';
480
                        if ($this->syntaxHighLight) {
481
                            $this->regHighLight('operator', $lineP, strlen($line) - 1);
482
                        }
483
                        if ($this->inBrace < 0) {
484
                            $this->error('Line ' . ($this->lineNumberOffset + $this->rawP - 1) . ': An end brace is in excess.', 1);
485
                            $this->inBrace = 0;
486
                        } else {
487
                            break;
488
                        }
489
                    } else {
490
                        if ($this->syntaxHighLight) {
491
                            $this->regHighLight('comment', $lineP);
492
                        }
493
                        // Comment. The comments are concatenated in this temporary string:
494
                        if ($this->regComments) {
495
                            $this->lastComment .= rtrim($line) . LF;
496
                        }
497
                    }
498
                    if (strpos($line, '### ERROR') === 0) {
499
                        $this->error(substr($line, 11));
500
                    }
501
                }
502
            }
503
            // Unset comment
504
            if ($this->commentSet) {
505
                if ($this->syntaxHighLight) {
506
                    $this->regHighLight('comment', $lineP);
507
                }
508
                if (strpos($line, '*/') === 0) {
509
                    $this->commentSet = 0;
510
                }
511
            }
512
        }
513
        return null;
514
    }
515
516
    /**
517
     * Executes operator functions, called from TypoScript
518
     * example: page.10.value := appendString(!)
519
     *
520
     * @param string $modifierName TypoScript function called
521
     * @param string $modifierArgument Function arguments; In case of multiple arguments, the method must split on its own
522
     * @param string $currentValue Current TypoScript value
523
     * @return string Modification result
524
     */
525
    protected function executeValueModifier($modifierName, $modifierArgument = null, $currentValue = null)
526
    {
527
        $newValue = null;
528
        switch ($modifierName) {
529
            case 'prependString':
530
                $newValue = $modifierArgument . $currentValue;
531
                break;
532
            case 'appendString':
533
                $newValue = $currentValue . $modifierArgument;
534
                break;
535
            case 'removeString':
536
                $newValue = str_replace($modifierArgument, '', $currentValue);
537
                break;
538
            case 'replaceString':
539
                list($fromStr, $toStr) = explode('|', $modifierArgument, 2);
540
                $newValue = str_replace($fromStr, $toStr, $currentValue);
541
                break;
542
            case 'addToList':
543
                $newValue = ((string)$currentValue !== '' ? $currentValue . ',' : '') . $modifierArgument;
544
                break;
545
            case 'removeFromList':
546
                $existingElements = GeneralUtility::trimExplode(',', $currentValue);
547
                $removeElements = GeneralUtility::trimExplode(',', $modifierArgument);
548
                if (!empty($removeElements)) {
549
                    $newValue = implode(',', array_diff($existingElements, $removeElements));
550
                }
551
                break;
552
            case 'uniqueList':
553
                $elements = GeneralUtility::trimExplode(',', $currentValue);
554
                $newValue = implode(',', array_unique($elements));
555
                break;
556
            case 'reverseList':
557
                $elements = GeneralUtility::trimExplode(',', $currentValue);
558
                $newValue = implode(',', array_reverse($elements));
559
                break;
560
            case 'sortList':
561
                $elements = GeneralUtility::trimExplode(',', $currentValue);
562
                $arguments = GeneralUtility::trimExplode(',', $modifierArgument);
563
                $arguments = array_map('strtolower', $arguments);
564
                $sort_flags = SORT_REGULAR;
565
                if (in_array('numeric', $arguments)) {
566
                    $sort_flags = SORT_NUMERIC;
567
                    // If the sorting modifier "numeric" is given, all values
568
                    // are checked and an exception is thrown if a non-numeric value is given
569
                    // otherwise there is a different behaviour between PHP7 and PHP 5.x
570
                    // See also the warning on http://us.php.net/manual/en/function.sort.php
571
                    foreach ($elements as $element) {
572
                        if (!is_numeric($element)) {
573
                            throw new \InvalidArgumentException('The list "' . $currentValue . '" should be sorted numerically but contains a non-numeric value', 1438191758);
574
                        }
575
                    }
576
                }
577
                sort($elements, $sort_flags);
578
                if (in_array('descending', $arguments)) {
579
                    $elements = array_reverse($elements);
580
                }
581
                $newValue = implode(',', $elements);
582
                break;
583
            default:
584
                if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName])) {
585
                    $hookMethod = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'][$modifierName];
586
                    $params = ['currentValue' => $currentValue, 'functionArgument' => $modifierArgument];
587
                    $fakeThis = false;
588
                    $newValue = GeneralUtility::callUserFunction($hookMethod, $params, $fakeThis);
589
                } else {
590
                    $this->getLogger()->warning('Missing function definition for ' . $modifierName . ' on TypoScript');
591
                }
592
        }
593
        return $newValue;
594
    }
595
596
    /**
597
     * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
598
     * thus having to recursively call itself to get the value
599
     *
600
     * @param string $string The object sub-path, eg "thisprop.another_prot
601
     * @param array $setup The local setup array from the function calling this function
602
     * @return string Returns the exitSignal
603
     * @see parseSub()
604
     */
605
    public function rollParseSub($string, array &$setup)
606
    {
607
        if ((string)$string === '') {
608
            return '';
609
        }
610
611
        list($key, $remainingKey) = $this->parseNextKeySegment($string);
612
        $key .= '.';
613
        if (!isset($setup[$key])) {
614
            $setup[$key] = [];
615
        }
616
        $exitSig = $remainingKey === ''
617
            ? $this->parseSub($setup[$key])
618
            : $this->rollParseSub($remainingKey, $setup[$key]);
619
        return $exitSig ?: '';
620
    }
621
622
    /**
623
     * Get a value/property pair for an object path in TypoScript, eg. "myobject.myvalue.mysubproperty".
624
     * Here: Used by the "copy" operator, <
625
     *
626
     * @param string $string Object path for which to get the value
627
     * @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.
628
     * @return array An array with keys 0/1 being value/property respectively
629
     */
630
    public function getVal($string, $setup)
631
    {
632
        if ((string)$string === '') {
633
            return [];
634
        }
635
636
        list($key, $remainingKey) = $this->parseNextKeySegment($string);
637
        $subKey = $key . '.';
638
        if ($remainingKey === '') {
639
            $retArr = [];
640
            if (isset($setup[$key])) {
641
                $retArr[0] = $setup[$key];
642
            }
643
            if (isset($setup[$subKey])) {
644
                $retArr[1] = $setup[$subKey];
645
            }
646
            return $retArr;
647
        }
648
        if ($setup[$subKey]) {
649
            return $this->getVal($remainingKey, $setup[$subKey]);
650
        }
651
652
        return [];
653
    }
654
655
    /**
656
     * Setting a value/property of an object string in the setup array.
657
     *
658
     * @param string $string The object sub-path, eg "thisprop.another_prot
659
     * @param array $setup The local setup array from the function calling this function.
660
     * @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)
661
     * @param bool $wipeOut If set, then both value and property is wiped out when a copy is made of another value.
662
     */
663
    public function setVal($string, array &$setup, $value, $wipeOut = false)
664
    {
665
        if ((string)$string === '') {
666
            return;
667
        }
668
669
        list($key, $remainingKey) = $this->parseNextKeySegment($string);
670
        $subKey = $key . '.';
671
        if ($remainingKey === '') {
672
            if ($value === 'UNSET') {
673
                unset($setup[$key]);
674
                unset($setup[$subKey]);
675
                if ($this->regLinenumbers) {
676
                    $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '>';
677
                }
678
            } else {
679
                $lnRegisDone = 0;
680
                if ($wipeOut) {
681
                    unset($setup[$key]);
682
                    unset($setup[$subKey]);
683
                    if ($this->regLinenumbers) {
684
                        $setup[$key . '.ln..'][] = ($this->lineNumberOffset + $this->rawP - 1) . '<';
685
                        $lnRegisDone = 1;
686
                    }
687
                }
688
                if (isset($value[0])) {
689
                    $setup[$key] = $value[0];
690
                }
691
                if (isset($value[1])) {
692
                    $setup[$subKey] = $value[1];
693
                }
694
                if ($this->lastComment && $this->regComments) {
695
                    $setup[$key . '..'] .= $this->lastComment;
696
                }
697
                if ($this->regLinenumbers && !$lnRegisDone) {
698
                    $setup[$key . '.ln..'][] = $this->lineNumberOffset + $this->rawP - 1;
699
                }
700
            }
701
        } else {
702
            if (!isset($setup[$subKey])) {
703
                $setup[$subKey] = [];
704
            }
705
            $this->setVal($remainingKey, $setup[$subKey], $value);
706
        }
707
    }
708
709
    /**
710
     * Determines the first key segment of a TypoScript key by searching for the first
711
     * unescaped dot in the given key string.
712
     *
713
     * Since the escape characters are only needed to correctly determine the key
714
     * segment any escape characters before the first unescaped dot are
715
     * stripped from the key.
716
     *
717
     * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
718
     * @return array Array with key segment and remaining part of $key
719
     */
720
    protected function parseNextKeySegment($key)
721
    {
722
        // if no dot is in the key, nothing to do
723
        $dotPosition = strpos($key, '.');
724
        if ($dotPosition === false) {
725
            return [$key, ''];
726
        }
727
728
        if (strpos($key, '\\') !== false) {
729
            // backslashes are in the key, so we do further parsing
730
731
            while ($dotPosition !== false) {
732
                if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
733
                    break;
734
                }
735
                // escaped dot found, continue
736
                $dotPosition = strpos($key, '.', $dotPosition + 1);
737
            }
738
739
            if ($dotPosition === false) {
740
                // no regular dot found
741
                $keySegment = $key;
742
                $remainingKey = '';
743
            } else {
744
                if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
745
                    $keySegment = substr($key, 0, $dotPosition - 1);
746
                } else {
747
                    $keySegment = substr($key, 0, $dotPosition);
748
                }
749
                $remainingKey = substr($key, $dotPosition + 1);
750
            }
751
752
            // fix key segment by removing escape sequences
753
            $keySegment = str_replace('\\.', '.', $keySegment);
754
        } else {
755
            // no backslash in the key, we're fine off
756
            list($keySegment, $remainingKey) = explode('.', $key, 2);
757
        }
758
        return [$keySegment, $remainingKey];
759
    }
760
761
    /**
762
     * Stacks errors/messages from the TypoScript parser into an internal array, $this->error
763
     * 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.
764
     *
765
     * @param string $err The error message string
766
     * @param int $num The error severity (in the scale of TimeTracker::setTSlogMessage: Approx: 2=warning, 1=info, 0=nothing, 3=fatal.)
767
     */
768
    public function error($err, $num = 2)
769
    {
770
        $tt = $this->getTimeTracker();
771
        if ($tt !== null) {
772
            $tt->setTSlogMessage($err, $num);
773
        }
774
        $this->errors[] = [$err, $num, $this->rawP - 1, $this->lineNumberOffset];
775
    }
776
777
    /**
778
     * Checks the input string (un-parsed TypoScript) for include-commands ("<INCLUDE_TYPOSCRIPT: ....")
779
     * Use: \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines()
780
     *
781
     * @param string $string Unparsed TypoScript
782
     * @param int $cycle_counter Counter for detecting endless loops
783
     * @param bool $returnFiles When set an array containing the resulting typoscript and all included files will get returned
784
     * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
785
     * @return string Complete TypoScript with includes added.
786
     * @static
787
     */
788
    public static function checkIncludeLines($string, $cycle_counter = 1, $returnFiles = false, $parentFilenameOrPath = '')
789
    {
790
        $includedFiles = [];
791
        if ($cycle_counter > 100) {
792
            self::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
793
            if ($returnFiles) {
794
                return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('typoscript...les' => $includedFiles) returns the type array<string,string|array> which is incompatible with the documented return type string.
Loading history...
795
                    'typoscript' => '',
796
                    'files' => $includedFiles
797
                ];
798
            }
799
            return '
800
###
801
### ERROR: Recursion!
802
###
803
';
804
        }
805
806
        // Checking for @import syntax imported files
807
        $string = self::addImportsFromExternalFiles($string, $cycle_counter, $returnFiles, $includedFiles, $parentFilenameOrPath);
808
809
        // If no tags found, no need to do slower preg_split
810
        if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== false) {
811
            $splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
812
            $parts = preg_split($splitRegEx, LF . $string . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
813
            // First text part goes through
814
            $newString = $parts[0] . LF;
815
            $partCount = count($parts);
0 ignored issues
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

815
            $partCount = count(/** @scrutinizer ignore-type */ $parts);
Loading history...
816
            for ($i = 1; $i + 3 < $partCount; $i += 4) {
817
                // $parts[$i] contains 'FILE' or 'DIR'
818
                // $parts[$i+1] contains relative file or directory path to be included
819
                // $parts[$i+2] optional properties of the INCLUDE statement
820
                // $parts[$i+3] next part of the typoscript string (part in between include-tags)
821
                $includeType = $parts[$i];
822
                $filename = $parts[$i + 1];
823
                $originalFilename = $filename;
824
                $optionalProperties = $parts[$i + 2];
825
                $tsContentsTillNextInclude = $parts[$i + 3];
826
827
                // Check condition
828
                $matches = preg_split('#(?i)condition\\s*=\\s*"((?:\\\\\\\\|\\\\"|[^\\"])*)"(\\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
829
                // If there was a condition
830
                if (count($matches) > 1) {
831
                    // Unescape the condition
832
                    $condition = trim(stripslashes($matches[1]));
833
                    // If necessary put condition in square brackets
834
                    if ($condition[0] !== '[') {
835
                        $condition = '[' . $condition . ']';
836
                    }
837
838
                    /** @var AbstractConditionMatcher $conditionMatcher */
839
                    $conditionMatcher = null;
840
                    if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE) {
841
                        $conditionMatcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class);
842
                    } else {
843
                        $conditionMatcher = GeneralUtility::makeInstance(BackendConditionMatcher::class);
844
                    }
845
846
                    // If it didn't match then proceed to the next include, but prepend next normal (not file) part to output string
847
                    if (!$conditionMatcher->match($condition)) {
848
                        $newString .= $tsContentsTillNextInclude . LF;
849
                        continue;
850
                    }
851
                }
852
853
                // Resolve a possible relative paths if a parent file is given
854
                if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
855
                    $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
856
                }
857
858
                // There must be a line-break char after - not sure why this check is necessary, kept it for being 100% backwards compatible
859
                // An empty string is also ok (means that the next line is also a valid include_typoscript tag)
860
                if (!preg_match('/(^\\s*\\r?\\n|^$)/', $tsContentsTillNextInclude)) {
861
                    $newString .= self::typoscriptIncludeError('Invalid characters after <INCLUDE_TYPOSCRIPT: source="' . $includeType . ':' . $filename . '">-tag (rest of line must be empty).');
862
                } elseif (strpos('..', $filename) !== false) {
863
                    $newString .= self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
864
                } else {
865
                    switch (strtolower($includeType)) {
866
                        case 'file':
867
                            self::includeFile($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
868
                            break;
869
                        case 'dir':
870
                            self::includeDirectory($originalFilename, $cycle_counter, $returnFiles, $newString, $includedFiles, $optionalProperties, $parentFilenameOrPath);
871
                            break;
872
                        default:
873
                            $newString .= self::typoscriptIncludeError('No valid option for INCLUDE_TYPOSCRIPT source property (valid options are FILE or DIR)');
874
                    }
875
                }
876
                // Prepend next normal (not file) part to output string
877
                $newString .= $tsContentsTillNextInclude . LF;
878
879
                // load default TypoScript for content rendering templates like
880
                // fluid_styled_content if those have been included through f.e.
881
                // <INCLUDE_TYPOSCRIPT: source="FILE:EXT:fluid_styled_content/Configuration/TypoScript/setup.txt">
882
                if (strpos(strtolower($filename), 'ext:') === 0) {
883
                    $filePointerPathParts = explode('/', substr($filename, 4));
884
885
                    // remove file part, determine whether to load setup or constants
886
                    list($includeType, ) = explode('.', array_pop($filePointerPathParts));
887
888
                    if (in_array($includeType, ['setup', 'constants'])) {
889
                        // adapt extension key to required format (no underscores)
890
                        $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
891
892
                        // load default TypoScript
893
                        $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
894
                        if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
895
                            $newString .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
896
                        }
897
                    }
898
                }
899
            }
900
            // Add a line break before and after the included code in order to make sure that the parser always has a LF.
901
            $string = LF . trim($newString) . LF;
902
        }
903
        // When all included files should get returned, simply return an compound array containing
904
        // the TypoScript with all "includes" processed and the files which got included
905
        if ($returnFiles) {
906
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('typoscript...les' => $includedFiles) returns the type array<string,string|array> which is incompatible with the documented return type string.
Loading history...
907
                'typoscript' => $string,
908
                'files' => $includedFiles
909
            ];
910
        }
911
        return $string;
912
    }
913
914
    /**
915
     * Splits the unparsed TypoScript content into @import statements
916
     *
917
     * @param string $typoScript unparsed TypoScript
918
     * @param int $cycleCounter counter to stop recursion
919
     * @param bool $returnFiles whether to populate the included Files or not
920
     * @param array $includedFiles - by reference - if any included files are added, they are added here
921
     * @param string $parentFilenameOrPath the current imported file to resolve relative paths - handled by reference
922
     * @return string the unparsed TypoScript with included external files
923
     */
924
    protected static function addImportsFromExternalFiles($typoScript, $cycleCounter, $returnFiles, &$includedFiles, &$parentFilenameOrPath)
925
    {
926
        // Check for new syntax "@import 'EXT:bennilove/Configuration/TypoScript/*'"
927
        if (strpos($typoScript, '@import \'') !== false || strpos($typoScript, '@import "') !== false) {
928
            $splitRegEx = '/\r?\n\s*@import\s[\'"]([^\'"]*)[\'"][\ \t]?/';
929
            $parts = preg_split($splitRegEx, LF . $typoScript . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
930
            // First text part goes through
931
            $newString = $parts[0] . LF;
932
            $partCount = count($parts);
0 ignored issues
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

932
            $partCount = count(/** @scrutinizer ignore-type */ $parts);
Loading history...
933
            for ($i = 1; $i + 2 <= $partCount; $i += 2) {
934
                $filename = $parts[$i];
935
                $tsContentsTillNextInclude = $parts[$i + 1];
936
                // Resolve a possible relative paths if a parent file is given
937
                if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
938
                    $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
939
                }
940
                $newString .= self::importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, $includedFiles);
941
                // Prepend next normal (not file) part to output string
942
                $newString .= $tsContentsTillNextInclude;
943
            }
944
            // Add a line break before and after the included code in order to make sure that the parser always has a LF.
945
            $typoScript = LF . trim($newString) . LF;
946
        }
947
        return $typoScript;
948
    }
949
950
    /**
951
     * Include file $filename. Contents of the file will be returned, filename is added to &$includedFiles.
952
     * Further include/import statements in the contents are processed recursively.
953
     *
954
     * @param string $filename Full absolute path+filename to the typoscript file to be included
955
     * @param int $cycleCounter Counter for detecting endless loops
956
     * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
957
     * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
958
     * @return string the unparsed TypoScript content from external files
959
     */
960
    protected static function importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, array &$includedFiles)
961
    {
962
        if (strpos('..', $filename) !== false) {
963
            return self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
964
        }
965
966
        $content = '';
967
        $absoluteFileName = GeneralUtility::getFileAbsFileName($filename);
968
        if ((string)$absoluteFileName === '') {
969
            return self::typoscriptIncludeError('Illegal filepath "' . $filename . '".');
970
        }
971
972
        $finder = new Finder();
973
        $finder
974
            // no recursive mode on purpose
975
            ->depth(0)
976
            // no directories should be fetched
977
            ->files()
978
            ->sortByName();
979
980
        // Search all files in the folder
981
        if (is_dir($absoluteFileName)) {
982
            $finder->in($absoluteFileName);
983
            // Used for the TypoScript comments
984
            $readableFilePrefix = $filename;
985
        } else {
986
            // Apparently this is not a folder, so the restriction
987
            // is the folder so we restrict into this folder
988
            $finder->in(dirname($absoluteFileName));
989
            if (!is_file($absoluteFileName)
990
                && strpos(basename($absoluteFileName), '*') === false
991
                && substr(basename($absoluteFileName), -11) !== '.typoscript') {
992
                $absoluteFileName .= '*.typoscript';
993
            }
994
            $finder->name(basename($absoluteFileName));
995
            $readableFilePrefix = dirname($filename);
996
        }
997
998
        foreach ($finder as $fileObject) {
999
            // Clean filename output for comments
1000
            $readableFileName = rtrim($readableFilePrefix, '/') . '/' . $fileObject->getFilename();
1001
            $content .= '### @import \'' . $readableFileName . '\' begin ###' . LF;
1002
            // Check for allowed files
1003
            if (!GeneralUtility::verifyFilenameAgainstDenyPattern($fileObject->getFilename())) {
1004
                $content .= self::typoscriptIncludeError('File "' . $readableFileName . '" was not included since it is not allowed due to fileDenyPattern.');
1005
            } else {
1006
                $includedFiles[] = $fileObject->getPathname();
1007
                // check for includes in included text
1008
                $included_text = self::checkIncludeLines($fileObject->getContents(), $cycleCounter++, $returnFiles, $absoluteFileName);
1009
                // If the method also has to return all included files, merge currently included
1010
                // files with files included by recursively calling itself
1011
                if ($returnFiles && is_array($included_text)) {
1012
                    $includedFiles = array_merge($includedFiles, $included_text['files']);
1013
                    $included_text = $included_text['typoscript'];
1014
                }
1015
                $content .= $included_text . LF;
1016
            }
1017
            $content .= '### @import \'' . $readableFileName . '\' end ###' . LF;
1018
1019
            // load default TypoScript for content rendering templates like
1020
            // fluid_styled_content if those have been included through e.g.
1021
            // @import "fluid_styled_content/Configuration/TypoScript/setup.typoscript"
1022
            if (strpos(strtoupper($filename), 'EXT:') === 0) {
1023
                $filePointerPathParts = explode('/', substr($filename, 4));
1024
                // remove file part, determine whether to load setup or constants
1025
                list($includeType) = explode('.', array_pop($filePointerPathParts));
1026
1027
                if (in_array($includeType, ['setup', 'constants'], true)) {
1028
                    // adapt extension key to required format (no underscores)
1029
                    $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
1030
1031
                    // load default TypoScript
1032
                    $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
1033
                    if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
1034
                        $content .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
1035
                    }
1036
                }
1037
            }
1038
        }
1039
1040
        if (empty($content)) {
1041
            return self::typoscriptIncludeError('No file or folder found for importing TypoScript on "' . $filename . '".');
1042
        }
1043
        return $content;
1044
    }
1045
1046
    /**
1047
     * Include file $filename. Contents of the file will be prepended to &$newstring, filename to &$includedFiles
1048
     * Further include_typoscript tags in the contents are processed recursively
1049
     *
1050
     * @param string $filename Relative path to the typoscript file to be included
1051
     * @param int $cycle_counter Counter for detecting endless loops
1052
     * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1053
     * @param string &$newString The output string to which the content of the file will be prepended (referenced
1054
     * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1055
     * @param string $optionalProperties
1056
     * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1057
     * @static
1058
     */
1059
    public static function includeFile($filename, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1060
    {
1061
        // Resolve a possible relative paths if a parent file is given
1062
        if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
1063
            $absfilename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
1064
        } else {
1065
            $absfilename = $filename;
1066
        }
1067
        $absfilename = GeneralUtility::getFileAbsFileName($absfilename);
1068
1069
        $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> BEGIN:' . LF;
1070
        if ((string)$filename !== '') {
1071
            // Must exist and must not contain '..' and must be relative
1072
            // Check for allowed files
1073
            if (!GeneralUtility::verifyFilenameAgainstDenyPattern($absfilename)) {
1074
                $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
1075
            } elseif (!@file_exists($absfilename)) {
1076
                $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not found.');
1077
            } else {
1078
                $includedFiles[] = $absfilename;
1079
                // check for includes in included text
1080
                $included_text = self::checkIncludeLines(file_get_contents($absfilename), $cycle_counter + 1, $returnFiles, $absfilename);
1081
                // If the method also has to return all included files, merge currently included
1082
                // files with files included by recursively calling itself
1083
                if ($returnFiles && is_array($included_text)) {
1084
                    $includedFiles = array_merge($includedFiles, $included_text['files']);
1085
                    $included_text = $included_text['typoscript'];
1086
                }
1087
                $newString .= $included_text . LF;
1088
            }
1089
        }
1090
        $newString .= '### <INCLUDE_TYPOSCRIPT: source="FILE:' . $filename . '"' . $optionalProperties . '> END:' . LF . LF;
1091
    }
1092
1093
    /**
1094
     * Include all files with matching Typoscript extensions in directory $dirPath. Contents of the files are
1095
     * prepended to &$newstring, filename to &$includedFiles.
1096
     * Order of the directory items to be processed: files first, then directories, both in alphabetical order.
1097
     * Further include_typoscript tags in the contents of the files are processed recursively.
1098
     *
1099
     * @param string $dirPath Relative path to the directory to be included
1100
     * @param int $cycle_counter Counter for detecting endless loops
1101
     * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
1102
     * @param string &$newString The output string to which the content of the file will be prepended (referenced)
1103
     * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
1104
     * @param string $optionalProperties
1105
     * @param string $parentFilenameOrPath The parent file (with absolute path) or path for relative includes
1106
     * @static
1107
     */
1108
    protected static function includeDirectory($dirPath, $cycle_counter = 1, $returnFiles = false, &$newString = '', array &$includedFiles = [], $optionalProperties = '', $parentFilenameOrPath = '')
1109
    {
1110
        // Extract the value of the property extensions="..."
1111
        $matches = preg_split('#(?i)extensions\s*=\s*"([^"]*)"(\s*|>)#', $optionalProperties, 2, PREG_SPLIT_DELIM_CAPTURE);
1112
        if (count($matches) > 1) {
0 ignored issues
show
Bug introduced by
It seems like $matches can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1112
        if (count(/** @scrutinizer ignore-type */ $matches) > 1) {
Loading history...
1113
            $includedFileExtensions = $matches[1];
1114
        } else {
1115
            $includedFileExtensions = '';
1116
        }
1117
1118
        // Resolve a possible relative paths if a parent file is given
1119
        if ($parentFilenameOrPath !== '' && $dirPath[0] === '.') {
1120
            $resolvedDirPath = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $dirPath);
1121
        } else {
1122
            $resolvedDirPath = $dirPath;
1123
        }
1124
        $absDirPath = GeneralUtility::getFileAbsFileName($resolvedDirPath);
1125
        if ($absDirPath) {
1126
            $absDirPath = rtrim($absDirPath, '/') . '/';
1127
            $newString .= LF . '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> BEGIN:' . LF;
1128
            // Get alphabetically sorted file index in array
1129
            $fileIndex = GeneralUtility::getAllFilesAndFoldersInPath([], $absDirPath, $includedFileExtensions);
1130
            // Prepend file contents to $newString
1131
            $prefixLength = strlen(PATH_site);
1132
            foreach ($fileIndex as $absFileRef) {
1133
                $relFileRef = substr($absFileRef, $prefixLength);
1134
                self::includeFile($relFileRef, $cycle_counter, $returnFiles, $newString, $includedFiles, '', $absDirPath);
1135
            }
1136
            $newString .= '### <INCLUDE_TYPOSCRIPT: source="DIR:' . $dirPath . '"' . $optionalProperties . '> END:' . LF . LF;
1137
        } else {
1138
            $newString .= self::typoscriptIncludeError('The path "' . $resolvedDirPath . '" is invalid.');
1139
        }
1140
    }
1141
1142
    /**
1143
     * Process errors in INCLUDE_TYPOSCRIPT tags
1144
     * Errors are logged and printed in the concatenated TypoScript result (as can be seen in Template Analyzer)
1145
     *
1146
     * @param string $error Text of the error message
1147
     * @return string The error message encapsulated in comments
1148
     * @static
1149
     */
1150
    protected static function typoscriptIncludeError($error)
1151
    {
1152
        self::getLogger()->warning($error);
1153
        return "\n###\n### ERROR: " . $error . "\n###\n\n";
1154
    }
1155
1156
    /**
1157
     * Parses the string in each value of the input array for include-commands
1158
     *
1159
     * @param array $array Array with TypoScript in each value
1160
     * @return array Same array but where the values has been parsed for include-commands
1161
     */
1162
    public static function checkIncludeLines_array(array $array)
1163
    {
1164
        foreach ($array as $k => $v) {
1165
            $array[$k] = self::checkIncludeLines($array[$k]);
1166
        }
1167
        return $array;
1168
    }
1169
1170
    /**
1171
     * Search for commented INCLUDE_TYPOSCRIPT statements
1172
     * and save the content between the BEGIN and the END line to the specified file
1173
     *
1174
     * @param string  $string Template content
1175
     * @param int $cycle_counter Counter for detecting endless loops
1176
     * @param array   $extractedFileNames
1177
     * @param string  $parentFilenameOrPath
1178
     *
1179
     * @throws \RuntimeException
1180
     * @throws \UnexpectedValueException
1181
     * @return string Template content with uncommented include statements
1182
     */
1183
    public static function extractIncludes($string, $cycle_counter = 1, array $extractedFileNames = [], $parentFilenameOrPath = '')
1184
    {
1185
        if ($cycle_counter > 10) {
1186
            self::getLogger()->warning('It appears like TypoScript code is looping over itself. Check your templates for "<INCLUDE_TYPOSCRIPT: ..." tags');
1187
            return '
1188
###
1189
### ERROR: Recursion!
1190
###
1191
';
1192
        }
1193
        $expectedEndTag = '';
1194
        $fileContent = [];
1195
        $restContent = [];
1196
        $fileName = null;
1197
        $inIncludePart = false;
1198
        $lines = preg_split("/\r\n|\n|\r/", $string);
1199
        $skipNextLineIfEmpty = false;
1200
        $openingCommentedIncludeStatement = null;
1201
        $optionalProperties = '';
1202
        foreach ($lines as $line) {
1203
            // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1204
            // an additional empty line, remove this again
1205
            if ($skipNextLineIfEmpty) {
1206
                if (trim($line) === '') {
1207
                    continue;
1208
                }
1209
                $skipNextLineIfEmpty = false;
1210
            }
1211
1212
            // Outside commented include statements
1213
            if (!$inIncludePart) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inIncludePart of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1214
                // Search for beginning commented include statements
1215
                if (preg_match('/###\\s*<INCLUDE_TYPOSCRIPT:\\s*source\\s*=\\s*"\\s*((?i)file|dir)\\s*:\\s*([^"]*)"(.*)>\\s*BEGIN/i', $line, $matches)) {
1216
                    // Found a commented include statement
1217
1218
                    // Save this line in case there is no ending tag
1219
                    $openingCommentedIncludeStatement = trim($line);
1220
                    $openingCommentedIncludeStatement = preg_replace('/\\s*### Warning: .*###\\s*/', '', $openingCommentedIncludeStatement);
1221
1222
                    // type of match: FILE or DIR
1223
                    $inIncludePart = strtoupper($matches[1]);
1224
                    $fileName = $matches[2];
1225
                    $optionalProperties = $matches[3];
1226
1227
                    $expectedEndTag = '### <INCLUDE_TYPOSCRIPT: source="' . $inIncludePart . ':' . $fileName . '"' . $optionalProperties . '> END';
1228
                    // Strip all whitespace characters to make comparison safer
1229
                    $expectedEndTag = strtolower(preg_replace('/\s/', '', $expectedEndTag));
1230
                } else {
1231
                    // If this is not a beginning commented include statement this line goes into the rest content
1232
                    $restContent[] = $line;
1233
                }
1234
            } else {
1235
                // Inside commented include statements
1236
                // Search for the matching ending commented include statement
1237
                $strippedLine = preg_replace('/\s/', '', $line);
1238
                if (stripos($strippedLine, $expectedEndTag) !== false) {
1239
                    // Found the matching ending include statement
1240
                    $fileContentString = implode(PHP_EOL, $fileContent);
1241
1242
                    // Write the content to the file
1243
1244
                    // Resolve a possible relative paths if a parent file is given
1245
                    if ($parentFilenameOrPath !== '' && $fileName[0] === '.') {
1246
                        $realFileName = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $fileName);
1247
                    } else {
1248
                        $realFileName = $fileName;
1249
                    }
1250
                    $realFileName = GeneralUtility::getFileAbsFileName($realFileName);
1251
1252
                    if ($inIncludePart === 'FILE') {
1253
                        // Some file checks
1254
                        if (!GeneralUtility::verifyFilenameAgainstDenyPattern($realFileName)) {
1255
                            throw new \UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
1256
                        }
1257
                        if (empty($realFileName)) {
1258
                            throw new \UnexpectedValueException(sprintf('"%s" is not a valid file location.', $fileName), 1294586441);
1259
                        }
1260
                        if (!is_writable($realFileName)) {
1261
                            throw new \RuntimeException(sprintf('"%s" is not writable.', $fileName), 1294586442);
1262
                        }
1263
                        if (in_array($realFileName, $extractedFileNames)) {
1264
                            throw new \RuntimeException(sprintf('Recursive/multiple inclusion of file "%s"', $realFileName), 1294586443);
1265
                        }
1266
                        $extractedFileNames[] = $realFileName;
1267
1268
                        // Recursive call to detected nested commented include statements
1269
                        $fileContentString = self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1270
1271
                        // Write the content to the file
1272
                        if (!GeneralUtility::writeFile($realFileName, $fileContentString)) {
1273
                            throw new \RuntimeException(sprintf('Could not write file "%s"', $realFileName), 1294586444);
1274
                        }
1275
                        // Insert reference to the file in the rest content
1276
                        $restContent[] = '<INCLUDE_TYPOSCRIPT: source="FILE:' . $fileName . '"' . $optionalProperties . '>';
1277
                    } else {
1278
                        // must be DIR
1279
1280
                        // Some file checks
1281
                        if (empty($realFileName)) {
1282
                            throw new \UnexpectedValueException(sprintf('"%s" is not a valid location.', $fileName), 1366493602);
1283
                        }
1284
                        if (!is_dir($realFileName)) {
1285
                            throw new \RuntimeException(sprintf('"%s" is not a directory.', $fileName), 1366493603);
1286
                        }
1287
                        if (in_array($realFileName, $extractedFileNames)) {
1288
                            throw new \RuntimeException(sprintf('Recursive/multiple inclusion of directory "%s"', $realFileName), 1366493604);
1289
                        }
1290
                        $extractedFileNames[] = $realFileName;
1291
1292
                        // Recursive call to detected nested commented include statements
1293
                        self::extractIncludes($fileContentString, $cycle_counter + 1, $extractedFileNames, $realFileName);
1294
1295
                        // just drop content between tags since it should usually just contain individual files from that dir
1296
1297
                        // Insert reference to the dir in the rest content
1298
                        $restContent[] = '<INCLUDE_TYPOSCRIPT: source="DIR:' . $fileName . '"' . $optionalProperties . '>';
1299
                    }
1300
1301
                    // Reset variables (preparing for the next commented include statement)
1302
                    $fileContent = [];
1303
                    $fileName = null;
1304
                    $inIncludePart = false;
1305
                    $openingCommentedIncludeStatement = null;
1306
                    // \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines inserts
1307
                    // an additional empty line, remove this again
1308
                    $skipNextLineIfEmpty = true;
1309
                } else {
1310
                    // If this is not an ending commented include statement this line goes into the file content
1311
                    $fileContent[] = $line;
1312
                }
1313
            }
1314
        }
1315
        // If we're still inside commented include statements copy the lines back to the rest content
1316
        if ($inIncludePart) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inIncludePart of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1317
            $restContent[] = $openingCommentedIncludeStatement . ' ### Warning: Corresponding end line missing! ###';
1318
            $restContent = array_merge($restContent, $fileContent);
1319
        }
1320
        $restContentString = implode(PHP_EOL, $restContent);
1321
        return $restContentString;
1322
    }
1323
1324
    /**
1325
     * Processes the string in each value of the input array with extractIncludes
1326
     *
1327
     * @param array $array Array with TypoScript in each value
1328
     * @return array Same array but where the values has been processed with extractIncludes
1329
     */
1330
    public static function extractIncludes_array(array $array)
1331
    {
1332
        foreach ($array as $k => $v) {
1333
            $array[$k] = self::extractIncludes($array[$k]);
1334
        }
1335
        return $array;
1336
    }
1337
1338
    /**********************************
1339
     *
1340
     * Syntax highlighting
1341
     *
1342
     *********************************/
1343
    /**
1344
     * Syntax highlight a TypoScript text
1345
     * Will parse the content. Remember, the internal setup array may contain invalid parsed content since conditions are ignored!
1346
     *
1347
     * @param string $string The TypoScript text
1348
     * @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.
1349
     * @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.
1350
     * @return string HTML code for the syntax highlighted string
1351
     */
1352
    public function doSyntaxHighlight($string, $lineNum = '', $highlightBlockMode = false)
1353
    {
1354
        $this->syntaxHighLight = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $syntaxHighLight was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1355
        $this->highLightData = [];
1356
        $this->errors = [];
1357
        // 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.
1358
        $string = str_replace(CR, '', $string);
1359
        $this->parse($string);
1360
        return $this->syntaxHighlight_print($lineNum, $highlightBlockMode);
1361
    }
1362
1363
    /**
1364
     * Registers a part of a TypoScript line for syntax highlighting.
1365
     *
1366
     * @param string $code Key from the internal array $this->highLightStyles
1367
     * @param int $pointer Pointer to the line in $this->raw which this is about
1368
     * @param int $strlen The number of chars LEFT on this line before the end is reached.
1369
     * @access private
1370
     * @see parse()
1371
     */
1372
    public function regHighLight($code, $pointer, $strlen = -1)
1373
    {
1374
        if ($strlen === -1) {
1375
            $this->highLightData[$pointer] = [[$code, 0]];
1376
        } else {
1377
            $this->highLightData[$pointer][] = [$code, $strlen];
1378
        }
1379
        $this->highLightData_bracelevel[$pointer] = $this->inBrace;
1380
    }
1381
1382
    /**
1383
     * Formatting the TypoScript code in $this->raw based on the data collected by $this->regHighLight in $this->highLightData
1384
     *
1385
     * @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.
1386
     * @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.
1387
     * @return string HTML content
1388
     * @access private
1389
     * @see doSyntaxHighlight()
1390
     */
1391
    public function syntaxHighlight_print($lineNumDat, $highlightBlockMode)
1392
    {
1393
        // Registers all error messages in relation to their linenumber
1394
        $errA = [];
1395
        foreach ($this->errors as $err) {
1396
            $errA[$err[2]][] = $err[0];
1397
        }
1398
        // Generates the syntax highlighted output:
1399
        $lines = [];
1400
        foreach ($this->raw as $rawP => $value) {
1401
            $start = 0;
1402
            $strlen = strlen($value);
1403
            $lineC = '';
1404
            if (is_array($this->highLightData[$rawP])) {
1405
                foreach ($this->highLightData[$rawP] as $set) {
1406
                    $len = $strlen - $start - $set[1];
1407
                    if ($len > 0) {
1408
                        $part = substr($value, $start, $len);
1409
                        $start += $len;
1410
                        $st = $this->highLightStyles[isset($this->highLightStyles[$set[0]]) ? $set[0] : 'default'];
1411
                        if (!$highlightBlockMode || $set[0] !== 'prespace') {
1412
                            $lineC .= $st[0] . htmlspecialchars($part) . $st[1];
1413
                        }
1414
                    } elseif ($len < 0) {
1415
                        debug([$len, $value, $rawP]);
1416
                    }
1417
                }
1418
            } else {
1419
                debug([$value]);
1420
            }
1421
            if (strlen($value) > $start) {
1422
                $lineC .= $this->highLightStyles['ignored'][0] . htmlspecialchars(substr($value, $start)) . $this->highLightStyles['ignored'][1];
1423
            }
1424
            if ($errA[$rawP]) {
1425
                $lineC .= $this->highLightStyles['error'][0] . '<strong> - ERROR:</strong> ' . htmlspecialchars(implode(';', $errA[$rawP])) . $this->highLightStyles['error'][1];
1426
            }
1427
            if ($highlightBlockMode && $this->highLightData_bracelevel[$rawP]) {
1428
                $lineC = str_pad('', $this->highLightData_bracelevel[$rawP] * 2, ' ', STR_PAD_LEFT) . '<span style="' . $this->highLightBlockStyles . ($this->highLightBlockStyles_basecolor ? 'background-color: ' . $this->modifyHTMLColorAll($this->highLightBlockStyles_basecolor, -$this->highLightData_bracelevel[$rawP] * 16) : '') . '">' . ($lineC !== '' ? $lineC : '&nbsp;') . '</span>';
1429
            }
1430
            if (is_array($lineNumDat)) {
1431
                $lineNum = $rawP + $lineNumDat[0];
1432
                if ($this->parentObject instanceof ExtendedTemplateService) {
1433
                    $lineNum = $this->parentObject->ext_lnBreakPointWrap($lineNum, $lineNum);
1434
                }
1435
                $lineC = $this->highLightStyles['linenum'][0] . str_pad($lineNum, 4, ' ', STR_PAD_LEFT) . ':' . $this->highLightStyles['linenum'][1] . ' ' . $lineC;
1436
            }
1437
            $lines[] = $lineC;
1438
        }
1439
        return '<pre class="ts-hl">' . implode(LF, $lines) . '</pre>';
1440
    }
1441
1442
    /**
1443
     * @return TimeTracker
1444
     */
1445
    protected function getTimeTracker()
1446
    {
1447
        return GeneralUtility::makeInstance(TimeTracker::class);
1448
    }
1449
1450
    /**
1451
     * Modifies a HTML Hex color by adding/subtracting $R,$G and $B integers
1452
     *
1453
     * @param string $color A hexadecimal color code, #xxxxxx
1454
     * @param int $R Offset value 0-255
1455
     * @param int $G Offset value 0-255
1456
     * @param int $B Offset value 0-255
1457
     * @return string A hexadecimal color code, #xxxxxx, modified according to input vars
1458
     * @see modifyHTMLColorAll()
1459
     */
1460
    protected function modifyHTMLColor($color, $R, $G, $B)
1461
    {
1462
        // This takes a hex-color (# included!) and adds $R, $G and $B to the HTML-color (format: #xxxxxx) and returns the new color
1463
        $nR = MathUtility::forceIntegerInRange(hexdec(substr($color, 1, 2)) + $R, 0, 255);
0 ignored issues
show
Bug introduced by
hexdec(substr($color, 1, 2)) + $R of type double is incompatible with the type integer expected by parameter $theInt of TYPO3\CMS\Core\Utility\M...::forceIntegerInRange(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1463
        $nR = MathUtility::forceIntegerInRange(/** @scrutinizer ignore-type */ hexdec(substr($color, 1, 2)) + $R, 0, 255);
Loading history...
1464
        $nG = MathUtility::forceIntegerInRange(hexdec(substr($color, 3, 2)) + $G, 0, 255);
1465
        $nB = MathUtility::forceIntegerInRange(hexdec(substr($color, 5, 2)) + $B, 0, 255);
1466
        return '#' . substr(('0' . dechex($nR)), -2) . substr(('0' . dechex($nG)), -2) . substr(('0' . dechex($nB)), -2);
1467
    }
1468
1469
    /**
1470
     * Modifies a HTML Hex color by adding/subtracting $all integer from all R/G/B channels
1471
     *
1472
     * @param string $color A hexadecimal color code, #xxxxxx
1473
     * @param int $all Offset value 0-255 for all three channels.
1474
     * @return string A hexadecimal color code, #xxxxxx, modified according to input vars
1475
     * @see modifyHTMLColor()
1476
     */
1477
    protected function modifyHTMLColorAll($color, $all)
1478
    {
1479
        return $this->modifyHTMLColor($color, $all, $all, $all);
1480
    }
1481
1482
    /**
1483
     * Get a logger instance
1484
     *
1485
     * This class uses logging mostly in static functions, hence we need a static getter for the logger.
1486
     * Injection of a logger instance via GeneralUtility::makeInstance is not possible.
1487
     *
1488
     * @return LoggerInterface
1489
     */
1490
    protected static function getLogger()
1491
    {
1492
        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
1493
    }
1494
}
1495