ContentObjectRenderer::getLanguageRestriction()   C
last analyzed

Complexity

Conditions 14
Paths 32

Size

Total Lines 48
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 48
rs 6.2666
c 0
b 0
f 0
cc 14
nc 32
nop 4

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\Frontend\ContentObject;
17
18
use Doctrine\DBAL\Exception as DBALException;
19
use Doctrine\DBAL\Statement;
20
use Psr\Container\ContainerInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Log\LoggerAwareInterface;
23
use Psr\Log\LoggerAwareTrait;
24
use Psr\Log\LogLevel;
25
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
26
use TYPO3\CMS\Core\Cache\CacheManager;
27
use TYPO3\CMS\Core\Configuration\Features;
28
use TYPO3\CMS\Core\Context\Context;
29
use TYPO3\CMS\Core\Context\LanguageAspect;
30
use TYPO3\CMS\Core\Core\Environment;
31
use TYPO3\CMS\Core\Database\ConnectionPool;
32
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
33
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
34
use TYPO3\CMS\Core\Database\Query\QueryHelper;
35
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
36
use TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction;
37
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
38
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
39
use TYPO3\CMS\Core\Html\HtmlParser;
40
use TYPO3\CMS\Core\Html\SanitizerBuilderFactory;
41
use TYPO3\CMS\Core\Html\SanitizerInitiator;
42
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
43
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
44
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
45
use TYPO3\CMS\Core\LinkHandling\LinkService;
46
use TYPO3\CMS\Core\Localization\Locales;
47
use TYPO3\CMS\Core\Log\LogManager;
48
use TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait;
49
use TYPO3\CMS\Core\Resource\Exception;
50
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
51
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
52
use TYPO3\CMS\Core\Resource\File;
53
use TYPO3\CMS\Core\Resource\FileInterface;
54
use TYPO3\CMS\Core\Resource\FileReference;
55
use TYPO3\CMS\Core\Resource\Folder;
56
use TYPO3\CMS\Core\Resource\ProcessedFile;
57
use TYPO3\CMS\Core\Resource\ResourceFactory;
58
use TYPO3\CMS\Core\Service\DependencyOrderingService;
59
use TYPO3\CMS\Core\Service\FlexFormService;
60
use TYPO3\CMS\Core\Site\SiteFinder;
61
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
62
use TYPO3\CMS\Core\Type\BitSet;
63
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
64
use TYPO3\CMS\Core\TypoScript\TemplateService;
65
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
66
use TYPO3\CMS\Core\Utility\ArrayUtility;
67
use TYPO3\CMS\Core\Utility\DebugUtility;
68
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
69
use TYPO3\CMS\Core\Utility\GeneralUtility;
70
use TYPO3\CMS\Core\Utility\HttpUtility;
71
use TYPO3\CMS\Core\Utility\MathUtility;
72
use TYPO3\CMS\Core\Utility\StringUtility;
73
use TYPO3\CMS\Core\Versioning\VersionState;
74
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
75
use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
76
use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
77
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
78
use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
79
use TYPO3\CMS\Frontend\Imaging\GifBuilder;
80
use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
81
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
82
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
83
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
84
use TYPO3\CMS\Frontend\Typolink\LinkResult;
85
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
86
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
87
use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
88
89
/**
90
 * This class contains all main TypoScript features.
91
 * This includes the rendering of TypoScript content objects (cObjects).
92
 * Is the backbone of TypoScript Template rendering.
93
 *
94
 * There are lots of functions you can use from your include-scripts.
95
 * The class is normally instantiated and referred to as "cObj".
96
 * When you call your own PHP-code typically through a USER or USER_INT cObject then it is this class that instantiates the object and calls the main method. Before it does so it will set (if you are using classes) a reference to itself in the internal variable "cObj" of the object. Thus you can access all functions and data from this class by $this->cObj->... from within you classes written to be USER or USER_INT content objects.
97
 */
98
class ContentObjectRenderer implements LoggerAwareInterface
99
{
100
    use LoggerAwareTrait;
101
    use DefaultJavaScriptAssetTrait;
102
103
    /**
104
     * @var ContainerInterface|null
105
     */
106
    protected $container;
107
108
    /**
109
     * @var array
110
     * @deprecated since v11, will be removed in v12. Unused.
111
     */
112
    public $align = [
113
        'center',
114
        'right',
115
        'left',
116
    ];
117
118
    /**
119
     * stdWrap functions in their correct order
120
     *
121
     * @see stdWrap()
122
     * @var string[]
123
     */
124
    public $stdWrapOrder = [
125
        'stdWrapPreProcess' => 'hook',
126
        // this is a placeholder for the first Hook
127
        'cacheRead' => 'hook',
128
        // this is a placeholder for checking if the content is available in cache
129
        'setContentToCurrent' => 'boolean',
130
        'setContentToCurrent.' => 'array',
131
        'addPageCacheTags' => 'string',
132
        'addPageCacheTags.' => 'array',
133
        'setCurrent' => 'string',
134
        'setCurrent.' => 'array',
135
        'lang.' => 'array',
136
        'data' => 'getText',
137
        'data.' => 'array',
138
        'field' => 'fieldName',
139
        'field.' => 'array',
140
        'current' => 'boolean',
141
        'current.' => 'array',
142
        'cObject' => 'cObject',
143
        'cObject.' => 'array',
144
        'numRows.' => 'array',
145
        'preUserFunc' => 'functionName',
146
        'stdWrapOverride' => 'hook',
147
        // this is a placeholder for the second Hook
148
        'override' => 'string',
149
        'override.' => 'array',
150
        'preIfEmptyListNum' => 'listNum',
151
        'preIfEmptyListNum.' => 'array',
152
        'ifNull' => 'string',
153
        'ifNull.' => 'array',
154
        'ifEmpty' => 'string',
155
        'ifEmpty.' => 'array',
156
        'ifBlank' => 'string',
157
        'ifBlank.' => 'array',
158
        'listNum' => 'listNum',
159
        'listNum.' => 'array',
160
        'trim' => 'boolean',
161
        'trim.' => 'array',
162
        'strPad.' => 'array',
163
        'stdWrap' => 'stdWrap',
164
        'stdWrap.' => 'array',
165
        'stdWrapProcess' => 'hook',
166
        // this is a placeholder for the third Hook
167
        'required' => 'boolean',
168
        'required.' => 'array',
169
        'if.' => 'array',
170
        'fieldRequired' => 'fieldName',
171
        'fieldRequired.' => 'array',
172
        'csConv' => 'string',
173
        'csConv.' => 'array',
174
        'parseFunc' => 'objectpath',
175
        'parseFunc.' => 'array',
176
        'HTMLparser' => 'boolean',
177
        'HTMLparser.' => 'array',
178
        'split.' => 'array',
179
        'replacement.' => 'array',
180
        'prioriCalc' => 'boolean',
181
        'prioriCalc.' => 'array',
182
        'char' => 'integer',
183
        'char.' => 'array',
184
        'intval' => 'boolean',
185
        'intval.' => 'array',
186
        'hash' => 'string',
187
        'hash.' => 'array',
188
        'round' => 'boolean',
189
        'round.' => 'array',
190
        'numberFormat.' => 'array',
191
        'expandList' => 'boolean',
192
        'expandList.' => 'array',
193
        'date' => 'dateconf',
194
        'date.' => 'array',
195
        'strtotime' => 'strtotimeconf',
196
        'strtotime.' => 'array',
197
        'strftime' => 'strftimeconf',
198
        'strftime.' => 'array',
199
        'age' => 'boolean',
200
        'age.' => 'array',
201
        'case' => 'case',
202
        'case.' => 'array',
203
        'bytes' => 'boolean',
204
        'bytes.' => 'array',
205
        'substring' => 'parameters',
206
        'substring.' => 'array',
207
        'cropHTML' => 'crop',
208
        'cropHTML.' => 'array',
209
        'stripHtml' => 'boolean',
210
        'stripHtml.' => 'array',
211
        'crop' => 'crop',
212
        'crop.' => 'array',
213
        'rawUrlEncode' => 'boolean',
214
        'rawUrlEncode.' => 'array',
215
        'htmlSpecialChars' => 'boolean',
216
        'htmlSpecialChars.' => 'array',
217
        'encodeForJavaScriptValue' => 'boolean',
218
        'encodeForJavaScriptValue.' => 'array',
219
        'doubleBrTag' => 'string',
220
        'doubleBrTag.' => 'array',
221
        'br' => 'boolean',
222
        'br.' => 'array',
223
        'brTag' => 'string',
224
        'brTag.' => 'array',
225
        'encapsLines.' => 'array',
226
        'keywords' => 'boolean',
227
        'keywords.' => 'array',
228
        'innerWrap' => 'wrap',
229
        'innerWrap.' => 'array',
230
        'innerWrap2' => 'wrap',
231
        'innerWrap2.' => 'array',
232
        'preCObject' => 'cObject',
233
        'preCObject.' => 'array',
234
        'postCObject' => 'cObject',
235
        'postCObject.' => 'array',
236
        'wrapAlign' => 'align',
237
        'wrapAlign.' => 'array',
238
        'typolink.' => 'array',
239
        'wrap' => 'wrap',
240
        'wrap.' => 'array',
241
        'noTrimWrap' => 'wrap',
242
        'noTrimWrap.' => 'array',
243
        'wrap2' => 'wrap',
244
        'wrap2.' => 'array',
245
        'dataWrap' => 'dataWrap',
246
        'dataWrap.' => 'array',
247
        'prepend' => 'cObject',
248
        'prepend.' => 'array',
249
        'append' => 'cObject',
250
        'append.' => 'array',
251
        'wrap3' => 'wrap',
252
        'wrap3.' => 'array',
253
        'orderedStdWrap' => 'stdWrap',
254
        'orderedStdWrap.' => 'array',
255
        'outerWrap' => 'wrap',
256
        'outerWrap.' => 'array',
257
        'insertData' => 'boolean',
258
        'insertData.' => 'array',
259
        'postUserFunc' => 'functionName',
260
        'postUserFuncInt' => 'functionName',
261
        'prefixComment' => 'string',
262
        'prefixComment.' => 'array',
263
        'editIcons' => 'string', // @deprecated since v11, will be removed with v12. Drop together with other editIcon removals.
264
        'editIcons.' => 'array', // @deprecated since v11, will be removed with v12. Drop together with other editIcon removals.
265
        'editPanel' => 'boolean', // @deprecated since v11, will be removed with v12. Drop together with other editPanel removals.
266
        'editPanel.' => 'array', // @deprecated since v11, will be removed with v12. Drop together with other editPanel removals.
267
        'htmlSanitize' => 'boolean',
268
        'htmlSanitize.' => 'array',
269
        'cacheStore' => 'hook',
270
        // this is a placeholder for storing the content in cache
271
        'stdWrapPostProcess' => 'hook',
272
        // this is a placeholder for the last Hook
273
        'debug' => 'boolean',
274
        'debug.' => 'array',
275
        'debugFunc' => 'boolean',
276
        'debugFunc.' => 'array',
277
        'debugData' => 'boolean',
278
        'debugData.' => 'array',
279
    ];
280
281
    /**
282
     * Class names for accordant content object names
283
     *
284
     * @var array
285
     */
286
    protected $contentObjectClassMap = [];
287
288
    /**
289
     * Loaded with the current data-record.
290
     *
291
     * If the instance of this class is used to render records from the database those records are found in this array.
292
     * The function stdWrap has TypoScript properties that fetch field-data from this array.
293
     *
294
     * @var array
295
     * @see start()
296
     */
297
    public $data = [];
298
299
    /**
300
     * @var string
301
     */
302
    protected $table = '';
303
304
    /**
305
     * Used for backup
306
     *
307
     * @var array
308
     * @deprecated since v11, will be removed in v12. Unused.
309
     */
310
    public $oldData = [];
311
312
    /**
313
     * If this is set with an array before stdWrap, it's used instead of $this->data in the data-property in stdWrap
314
     *
315
     * @var string
316
     * @deprecated since v11, will be removed in v12. Drop together with usages in this class.
317
     */
318
    public $alternativeData = '';
319
320
    /**
321
     * Used by the parseFunc function and is loaded with tag-parameters when parsing tags.
322
     *
323
     * @var array
324
     */
325
    public $parameters = [];
326
327
    /**
328
     * @var string
329
     */
330
    public $currentValKey = 'currentValue_kidjls9dksoje';
331
332
    /**
333
     * This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation.
334
     * Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered.
335
     *
336
     * @var string
337
     */
338
    public $currentRecord = '';
339
340
    /**
341
     * Set in RecordsContentObject and ContentContentObject to the current number of records selected in a query.
342
     *
343
     * @var int
344
     * @deprecated since v11, will be removed in v12. Drop together with usages in RecordsContentObject and ContentContentObject
345
     */
346
    public $currentRecordTotal = 0;
347
348
    /**
349
     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
350
     *
351
     * @var int
352
     */
353
    public $currentRecordNumber = 0;
354
355
    /**
356
     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
357
     *
358
     * @var int
359
     */
360
    public $parentRecordNumber = 0;
361
362
    /**
363
     * If the ContentObjectRender was started from ContentContentObject, RecordsContentObject or SearchResultContentObject this array has two keys, 'data' and 'currentRecord' which indicates the record and data for the parent cObj.
364
     *
365
     * @var array
366
     */
367
    public $parentRecord = [];
368
369
    /**
370
     * @var string|int
371
     */
372
    public $checkPid_badDoktypeList = PageRepository::DOKTYPE_RECYCLER;
373
374
    /**
375
     * This will be set by typoLink() to the url of the most recent link created.
376
     *
377
     * @var string
378
     */
379
    public $lastTypoLinkUrl = '';
380
381
    /**
382
     * DO. link target.
383
     *
384
     * @var string
385
     */
386
    public $lastTypoLinkTarget = '';
387
388
    /**
389
     * @var array
390
     */
391
    public $lastTypoLinkLD = [];
392
393
    public ?LinkResultInterface $lastTypoLinkResult = null;
394
395
    /**
396
     * array that registers rendered content elements (or any table) to make sure they are not rendered recursively!
397
     *
398
     * @var array
399
     * @deprecated since v11, will be removed in v12. Unused.
400
     */
401
    public $recordRegister = [];
402
403
    /**
404
     * Containing hook objects for stdWrap
405
     *
406
     * @var array
407
     */
408
    protected $stdWrapHookObjects = [];
409
410
    /**
411
     * Containing hook objects for getImgResource
412
     *
413
     * @var array
414
     */
415
    protected $getImgResourceHookObjects;
416
417
    /**
418
     * @var File|FileReference|Folder|string|null Current file objects (during iterations over files)
419
     */
420
    protected $currentFile;
421
422
    /**
423
     * Set to TRUE by doConvertToUserIntObject() if USER object wants to become USER_INT
424
     * @var bool
425
     */
426
    public $doConvertToUserIntObject = false;
427
428
    /**
429
     * Indicates current object type. Can hold one of OBJECTTYPE_ constants or FALSE.
430
     * The value is set and reset inside USER() function. Any time outside of
431
     * USER() it is FALSE.
432
     * @var bool
433
     */
434
    protected $userObjectType = false;
435
436
    /**
437
     * @var array
438
     */
439
    protected $stopRendering = [];
440
441
    /**
442
     * @var int
443
     */
444
    protected $stdWrapRecursionLevel = 0;
445
446
    /**
447
     * @var TypoScriptFrontendController|null
448
     */
449
    protected $typoScriptFrontendController;
450
451
    /**
452
     * Request pointer, if injected. Use getRequest() instead of reading this property directly.
453
     *
454
     * @var ServerRequestInterface|null
455
     */
456
    private ?ServerRequestInterface $request = null;
457
458
    /**
459
     * Indicates that object type is USER.
460
     *
461
     * @see ContentObjectRender::$userObjectType
462
     */
463
    const OBJECTTYPE_USER_INT = 1;
464
    /**
465
     * Indicates that object type is USER.
466
     *
467
     * @see ContentObjectRender::$userObjectType
468
     */
469
    const OBJECTTYPE_USER = 2;
470
471
    /**
472
     * @param TypoScriptFrontendController $typoScriptFrontendController
473
     * @param ContainerInterface $container
474
     */
475
    public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null, ContainerInterface $container = null)
476
    {
477
        $this->typoScriptFrontendController = $typoScriptFrontendController;
478
        $this->contentObjectClassMap = $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] ?? [];
479
        $this->container = $container;
480
    }
481
482
    public function setRequest(ServerRequestInterface $request): void
483
    {
484
        $this->request = $request;
485
    }
486
487
    /**
488
     * Prevent several objects from being serialized.
489
     * If currentFile is set, it is either a File or a FileReference object. As the object itself can't be serialized,
490
     * we have store a hash and restore the object in __wakeup()
491
     *
492
     * @return array
493
     */
494
    public function __sleep()
495
    {
496
        $vars = get_object_vars($this);
497
        unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container'], $vars['request']);
498
        if ($this->currentFile instanceof FileReference) {
499
            $this->currentFile = 'FileReference:' . $this->currentFile->getUid();
500
        } elseif ($this->currentFile instanceof File) {
501
            $this->currentFile = 'File:' . $this->currentFile->getIdentifier();
502
        } else {
503
            unset($vars['currentFile']);
504
        }
505
        return array_keys($vars);
506
    }
507
508
    /**
509
     * Restore currentFile from hash.
510
     * If currentFile references a File, the identifier equals file identifier.
511
     * If it references a FileReference the identifier equals the uid of the reference.
512
     */
513
    public function __wakeup()
514
    {
515
        if (isset($GLOBALS['TSFE'])) {
516
            $this->typoScriptFrontendController = $GLOBALS['TSFE'];
517
        }
518
        if ($this->currentFile !== null && is_string($this->currentFile)) {
519
            [$objectType, $identifier] = explode(':', $this->currentFile, 2);
520
            try {
521
                if ($objectType === 'File') {
522
                    $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($identifier);
523
                } elseif ($objectType === 'FileReference') {
524
                    $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject($identifier);
525
                }
526
            } catch (ResourceDoesNotExistException $e) {
527
                $this->currentFile = null;
528
            }
529
        }
530
        $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
531
        $this->container = GeneralUtility::getContainer();
532
533
        // We do not derive $this->request from globals here. The request is expected to be injected
534
        // using setRequest() after deserialization or with start().
535
        // (A fallback to $GLOBALS['TYPO3_REQUEST'] is available in getRequest() for BC)
536
    }
537
538
    /**
539
     * Allow injecting content object class map.
540
     *
541
     * This method is private API, please use configuration
542
     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
543
     *
544
     * @internal
545
     * @param array $contentObjectClassMap
546
     */
547
    public function setContentObjectClassMap(array $contentObjectClassMap)
548
    {
549
        $this->contentObjectClassMap = $contentObjectClassMap;
550
    }
551
552
    /**
553
     * Register a single content object name to class name
554
     *
555
     * This method is private API, please use configuration
556
     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
557
     *
558
     * @param string $className
559
     * @param string $contentObjectName
560
     * @internal
561
     */
562
    public function registerContentObjectClass($className, $contentObjectName)
563
    {
564
        $this->contentObjectClassMap[$contentObjectName] = $className;
565
    }
566
567
    /**
568
     * Class constructor.
569
     * Well, it has to be called manually since it is not a real constructor function.
570
     * So after making an instance of the class, call this function and pass to it a database record and the tablename from where the record is from. That will then become the "current" record loaded into memory and accessed by the .fields property found in eg. stdWrap.
571
     *
572
     * @param array $data The record data that is rendered.
573
     * @param string $table The table that the data record is from.
574
     * @param ServerRequestInterface|null $request
575
     */
576
    public function start($data, $table = '', ?ServerRequestInterface $request = null)
577
    {
578
        $this->request = $request ?? $this->request;
579
        $this->data = $data;
580
        $this->table = $table;
581
        $this->currentRecord = $table !== ''
582
            ? $table . ':' . ($this->data['uid'] ?? '')
583
            : '';
584
        $this->parameters = [];
585
        $this->stdWrapHookObjects = [];
586
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] ?? [] as $className) {
587
            $hookObject = GeneralUtility::makeInstance($className);
588
            if (!$hookObject instanceof ContentObjectStdWrapHookInterface) {
589
                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectStdWrapHookInterface::class, 1195043965);
590
            }
591
            $this->stdWrapHookObjects[] = $hookObject;
592
        }
593
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] ?? [] as $className) {
594
            $postInitializationProcessor = GeneralUtility::makeInstance($className);
595
            if (!$postInitializationProcessor instanceof ContentObjectPostInitHookInterface) {
596
                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectPostInitHookInterface::class, 1274563549);
597
            }
598
            $postInitializationProcessor->postProcessContentObjectInitialization($this);
599
        }
600
    }
601
602
    /**
603
     * Returns the current table
604
     *
605
     * @return string
606
     */
607
    public function getCurrentTable()
608
    {
609
        return $this->table;
610
    }
611
612
    /**
613
     * Gets the 'getImgResource' hook objects.
614
     * The first call initializes the accordant objects.
615
     *
616
     * @return array The 'getImgResource' hook objects (if any)
617
     */
618
    protected function getGetImgResourceHookObjects()
619
    {
620
        if (!isset($this->getImgResourceHookObjects)) {
621
            $this->getImgResourceHookObjects = [];
622
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] ?? [] as $className) {
623
                $hookObject = GeneralUtility::makeInstance($className);
624
                if (!$hookObject instanceof ContentObjectGetImageResourceHookInterface) {
625
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetImageResourceHookInterface::class, 1218636383);
626
                }
627
                $this->getImgResourceHookObjects[] = $hookObject;
628
            }
629
        }
630
        return $this->getImgResourceHookObjects;
631
    }
632
633
    /**
634
     * Sets the internal variable parentRecord with information about current record.
635
     * If the ContentObjectRender was started from CONTENT, RECORD or SEARCHRESULT cObject's this array has two keys, 'data' and 'currentRecord' which indicates the record and data for the parent cObj.
636
     *
637
     * @param array $data The record array
638
     * @param string $currentRecord This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation. Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered.
639
     * @internal
640
     */
641
    public function setParent($data, $currentRecord)
642
    {
643
        $this->parentRecord = [
644
            'data' => $data,
645
            'currentRecord' => $currentRecord,
646
        ];
647
    }
648
649
    /***********************************************
650
     *
651
     * CONTENT_OBJ:
652
     *
653
     ***********************************************/
654
    /**
655
     * Returns the "current" value.
656
     * The "current" value is just an internal variable that can be used by functions to pass a single value on to another function later in the TypoScript processing.
657
     * It's like "load accumulator" in the good old C64 days... basically a "register" you can use as you like.
658
     * The TSref will tell if functions are setting this value before calling some other object so that you know if it holds any special information.
659
     *
660
     * @return mixed The "current" value
661
     */
662
    public function getCurrentVal()
663
    {
664
        return $this->data[$this->currentValKey];
665
    }
666
667
    /**
668
     * Sets the "current" value.
669
     *
670
     * @param mixed $value The variable that you want to set as "current
671
     * @see getCurrentVal()
672
     */
673
    public function setCurrentVal($value)
674
    {
675
        $this->data[$this->currentValKey] = $value;
676
    }
677
678
    /**
679
     * Rendering of a "numerical array" of cObjects from TypoScript
680
     * Will call ->cObjGetSingle() for each cObject found and accumulate the output.
681
     *
682
     * @param array $setup array with cObjects as values.
683
     * @param string $addKey A prefix for the debugging information
684
     * @return string Rendered output from the cObjects in the array.
685
     * @see cObjGetSingle()
686
     */
687
    public function cObjGet($setup, $addKey = '')
688
    {
689
        if (!is_array($setup)) {
0 ignored issues
show
introduced by
The condition is_array($setup) is always true.
Loading history...
690
            return '';
691
        }
692
        $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($setup);
693
        $content = '';
694
        foreach ($sKeyArray as $theKey) {
695
            $theValue = $setup[$theKey];
696
            if ((int)$theKey && !str_contains($theKey, '.')) {
697
                $conf = $setup[$theKey . '.'] ?? [];
698
                $content .= $this->cObjGetSingle($theValue, $conf, $addKey . $theKey);
699
            }
700
        }
701
        return $content;
702
    }
703
704
    /**
705
     * Renders a content object
706
     *
707
     * @param string $name The content object name, eg. "TEXT" or "USER" or "IMAGE"
708
     * @param array $conf The array with TypoScript properties for the content object
709
     * @param string $TSkey A string label used for the internal debugging tracking.
710
     * @return string cObject output
711
     * @throws \UnexpectedValueException
712
     */
713
    public function cObjGetSingle($name, $conf, $TSkey = '__')
714
    {
715
        $content = '';
716
        $timeTracker = $this->getTimeTracker();
717
        $name = trim($name);
718
        if ($timeTracker->LR) {
719
            $timeTracker->push($TSkey, $name);
720
        }
721
        // Checking if the COBJ is a reference to another object. (eg. name of 'some.object =< styles.something')
722
        if (isset($name[0]) && $name[0] === '<') {
723
            $key = trim(substr($name, 1));
724
            $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
725
            // $name and $conf is loaded with the referenced values.
726
            $confOverride = is_array($conf) ? $conf : [];
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
727
            [$name, $conf] = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
728
            $conf = array_replace_recursive($conf, $confOverride);
729
            // Getting the cObject
730
            $timeTracker->incStackPointer();
731
            $content .= $this->cObjGetSingle($name, $conf, $key);
732
            $timeTracker->decStackPointer();
733
        } else {
734
            $contentObject = $this->getContentObject($name);
735
            if ($contentObject) {
736
                $content .= $this->render($contentObject, $conf);
737
            }
738
        }
739
        if ($timeTracker->LR) {
740
            $timeTracker->pull($content);
741
        }
742
        return $content;
743
    }
744
745
    /**
746
     * Returns a new content object of type $name.
747
     * This content object needs to be registered as content object
748
     * in $this->contentObjectClassMap
749
     *
750
     * @param string $name
751
     * @return AbstractContentObject|null
752
     * @throws ContentRenderingException
753
     */
754
    public function getContentObject($name)
755
    {
756
        if (!isset($this->contentObjectClassMap[$name])) {
757
            return null;
758
        }
759
        $fullyQualifiedClassName = $this->contentObjectClassMap[$name];
760
        $contentObject = GeneralUtility::makeInstance($fullyQualifiedClassName, $this);
761
        if (!($contentObject instanceof AbstractContentObject)) {
762
            throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295);
763
        }
764
        $contentObject->setRequest($this->getRequest());
765
        return $contentObject;
766
    }
767
768
    /********************************************
769
     *
770
     * Functions rendering content objects (cObjects)
771
     *
772
     ********************************************/
773
    /**
774
     * Renders a content object by taking exception and cache handling
775
     * into consideration
776
     *
777
     * @param AbstractContentObject $contentObject Content object instance
778
     * @param array $configuration Array of TypoScript properties
779
     *
780
     * @throws ContentRenderingException
781
     * @throws \Exception
782
     * @return string
783
     */
784
    public function render(AbstractContentObject $contentObject, $configuration = [])
785
    {
786
        $content = '';
787
788
        // Evaluate possible cache and return
789
        $cacheConfiguration = $configuration['cache.'] ?? null;
790
        if ($cacheConfiguration !== null) {
791
            unset($configuration['cache.']);
792
            $cache = $this->getFromCache($cacheConfiguration);
793
            if ($cache !== false) {
794
                return $cache;
795
            }
796
        }
797
798
        // Render content
799
        try {
800
            $content .= $contentObject->render($configuration);
801
        } catch (ContentRenderingException $exception) {
802
            // Content rendering Exceptions indicate a critical problem which should not be
803
            // caught e.g. when something went wrong with Exception handling itself
804
            throw $exception;
805
        } catch (\Exception $exception) {
806
            $exceptionHandler = $this->createExceptionHandler($configuration);
807
            if ($exceptionHandler === null) {
808
                throw $exception;
809
            }
810
            $content = $exceptionHandler->handle($exception, $contentObject, $configuration);
811
        }
812
813
        // Store cache
814
        if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) {
815
            $key = $this->calculateCacheKey($cacheConfiguration);
816
            if (!empty($key)) {
817
                /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
818
                $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
819
                $tags = $this->calculateCacheTags($cacheConfiguration);
820
                $lifetime = $this->calculateCacheLifetime($cacheConfiguration);
821
                $cacheFrontend->set($key, $content, $tags, $lifetime);
822
            }
823
        }
824
825
        return $content;
826
    }
827
828
    /**
829
     * Creates the content object exception handler from local content object configuration
830
     * or, from global configuration if not explicitly disabled in local configuration
831
     *
832
     * @param array $configuration
833
     * @return ExceptionHandlerInterface|null
834
     * @throws ContentRenderingException
835
     */
836
    protected function createExceptionHandler($configuration = [])
837
    {
838
        $exceptionHandler = null;
839
        $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration);
840
        if (!empty($exceptionHandlerClassName)) {
841
            if (method_exists($exceptionHandlerClassName, 'setConfiguration')) {
842
                $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName);
843
                $exceptionHandler->setConfiguration($this->mergeExceptionHandlerConfiguration($configuration));
844
            } else {
845
                trigger_error(
846
                    'Passing the TypoScript configuration as constructor argument to ' . $exceptionHandlerClassName . ' is deprecated and will stop working in TYPO3 v12.0. Exception handler classes therefore have to implement the setConfiguration() method.',
847
                    E_USER_DEPRECATED
848
                );
849
                $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName, $this->mergeExceptionHandlerConfiguration($configuration));
850
            }
851
            if (!$exceptionHandler instanceof ExceptionHandlerInterface) {
852
                throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369);
853
            }
854
        }
855
856
        return $exceptionHandler;
857
    }
858
859
    /**
860
     * Determine exception handler class name from global and content object configuration
861
     *
862
     * @param array $configuration
863
     * @return string|null
864
     */
865
    protected function determineExceptionHandlerClassName($configuration)
866
    {
867
        $exceptionHandlerClassName = null;
868
        $tsfe = $this->getTypoScriptFrontendController();
869
        if (!isset($tsfe->config['config']['contentObjectExceptionHandler'])) {
870
            if (Environment::getContext()->isProduction()) {
871
                $exceptionHandlerClassName = '1';
872
            }
873
        } else {
874
            $exceptionHandlerClassName = $tsfe->config['config']['contentObjectExceptionHandler'];
875
        }
876
877
        if (isset($configuration['exceptionHandler'])) {
878
            $exceptionHandlerClassName = $configuration['exceptionHandler'];
879
        }
880
881
        if ($exceptionHandlerClassName === '1') {
882
            $exceptionHandlerClassName = ProductionExceptionHandler::class;
883
        }
884
885
        return $exceptionHandlerClassName;
886
    }
887
888
    /**
889
     * Merges global exception handler configuration with the one from the content object
890
     * and returns the merged exception handler configuration
891
     *
892
     * @param array $configuration
893
     * @return array
894
     */
895
    protected function mergeExceptionHandlerConfiguration($configuration)
896
    {
897
        $exceptionHandlerConfiguration = [];
898
        $tsfe = $this->getTypoScriptFrontendController();
899
        if (!empty($tsfe->config['config']['contentObjectExceptionHandler.'])) {
900
            $exceptionHandlerConfiguration = $tsfe->config['config']['contentObjectExceptionHandler.'];
901
        }
902
        if (!empty($configuration['exceptionHandler.'])) {
903
            $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']);
904
        }
905
906
        return $exceptionHandlerConfiguration;
907
    }
908
909
    /**
910
     * Retrieves a type of object called as USER or USER_INT. Object can detect their
911
     * type by using this call. It returns OBJECTTYPE_USER_INT or OBJECTTYPE_USER depending on the
912
     * current object execution. In all other cases it will return FALSE to indicate
913
     * a call out of context.
914
     *
915
     * @return mixed One of OBJECTTYPE_ class constants or FALSE
916
     */
917
    public function getUserObjectType()
918
    {
919
        return $this->userObjectType;
920
    }
921
922
    /**
923
     * Sets the user object type
924
     *
925
     * @param mixed $userObjectType
926
     */
927
    public function setUserObjectType($userObjectType)
928
    {
929
        $this->userObjectType = $userObjectType;
930
    }
931
932
    /**
933
     * Requests the current USER object to be converted to USER_INT.
934
     */
935
    public function convertToUserIntObject()
936
    {
937
        if ($this->userObjectType !== self::OBJECTTYPE_USER) {
0 ignored issues
show
introduced by
The condition $this->userObjectType !== self::OBJECTTYPE_USER is always true.
Loading history...
938
            $this->getTimeTracker()->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', LogLevel::WARNING);
939
        } else {
940
            $this->doConvertToUserIntObject = true;
941
        }
942
    }
943
944
    /************************************
945
     *
946
     * Various helper functions for content objects:
947
     *
948
     ************************************/
949
    /**
950
     * Converts a given config in Flexform to a conf-array
951
     *
952
     * @param string|array $flexData Flexform data
953
     * @param array $conf Array to write the data into, by reference
954
     * @param bool $recursive Is set if called recursive. Don't call function with this parameter, it's used inside the function only
955
     */
956
    public function readFlexformIntoConf($flexData, &$conf, $recursive = false)
957
    {
958
        if ($recursive === false && is_string($flexData)) {
959
            $flexData = GeneralUtility::xml2array($flexData, 'T3');
960
        }
961
        if (is_array($flexData) && isset($flexData['data']['sDEF']['lDEF'])) {
962
            $flexData = $flexData['data']['sDEF']['lDEF'];
963
        }
964
        if (!is_array($flexData)) {
965
            return;
966
        }
967
        foreach ($flexData as $key => $value) {
968
            if (!is_array($value)) {
969
                continue;
970
            }
971
            if (isset($value['el'])) {
972
                if (is_array($value['el']) && !empty($value['el'])) {
973
                    foreach ($value['el'] as $ekey => $element) {
974
                        if (isset($element['vDEF'])) {
975
                            $conf[$ekey] = $element['vDEF'];
976
                        } else {
977
                            if (is_array($element)) {
978
                                $this->readFlexformIntoConf($element, $conf[$key][key($element)][$ekey], true);
979
                            } else {
980
                                $this->readFlexformIntoConf($element, $conf[$key][$ekey], true);
981
                            }
982
                        }
983
                    }
984
                } else {
985
                    $this->readFlexformIntoConf($value['el'], $conf[$key], true);
986
                }
987
            }
988
            if (isset($value['vDEF'])) {
989
                $conf[$key] = $value['vDEF'];
990
            }
991
        }
992
    }
993
994
    /**
995
     * Returns all parents of the given PID (Page UID) list
996
     *
997
     * @param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap
998
     * @param array $pidConf stdWrap array for the list
999
     * @return string A list of PIDs
1000
     * @internal
1001
     */
1002
    public function getSlidePids($pidList, $pidConf)
1003
    {
1004
        // todo: phpstan states that $pidConf always exists and is not nullable. At the moment, this is a false positive
1005
        //       as null can be passed into this method via $pidConf. As soon as more strict types are used, this isset
1006
        //       check must be replaced with a more appropriate check like empty or count.
1007
        $pidList = isset($pidConf) ? trim($this->stdWrap($pidList, $pidConf)) : trim($pidList);
1008
        if ($pidList === '') {
1009
            $pidList = 'this';
1010
        }
1011
        $tsfe = $this->getTypoScriptFrontendController();
1012
        $listArr = null;
1013
        if (trim($pidList)) {
1014
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $pidList));
1015
            $listArr = $this->checkPidArray($listArr);
1016
        }
1017
        $pidList = [];
1018
        if (is_array($listArr) && !empty($listArr)) {
1019
            foreach ($listArr as $uid) {
1020
                $page = $tsfe->sys_page->getPage($uid);
1021
                if (!$page['is_siteroot']) {
1022
                    $pidList[] = $page['pid'];
1023
                }
1024
            }
1025
        }
1026
        return implode(',', $pidList);
1027
    }
1028
1029
    /**
1030
     * Wraps the input string in link-tags that opens the image in a new window.
1031
     *
1032
     * @param string $string String to wrap, probably an <img> tag
1033
     * @param string|File|FileReference $imageFile The original image file
1034
     * @param array $conf TypoScript properties for the "imageLinkWrap" function
1035
     * @return string The input string, $string, wrapped as configured.
1036
     * @internal This method should be used within TYPO3 Core only
1037
     */
1038
    public function imageLinkWrap($string, $imageFile, $conf)
1039
    {
1040
        $string = (string)$string;
1041
        $enable = $this->stdWrapValue('enable', $conf ?? []);
1042
        if (!$enable) {
1043
            return $string;
1044
        }
1045
        $content = (string)$this->typoLink($string, $conf['typolink.'] ?? []);
1046
        if (isset($conf['file.']) && is_scalar($imageFile)) {
1047
            $imageFile = $this->stdWrap((string)$imageFile, $conf['file.']);
1048
        }
1049
1050
        if ($imageFile instanceof File) {
1051
            $file = $imageFile;
1052
        } elseif ($imageFile instanceof FileReference) {
1053
            $file = $imageFile->getOriginalFile();
1054
        } else {
1055
            if (MathUtility::canBeInterpretedAsInteger($imageFile)) {
1056
                $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$imageFile);
1057
            } else {
1058
                $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObjectFromCombinedIdentifier($imageFile);
1059
            }
1060
        }
1061
1062
        // Create imageFileLink if not created with typolink
1063
        if ($content === $string && $file !== null) {
1064
            $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop'];
1065
            $parameters = [];
1066
            $sample = $this->stdWrapValue('sample', $conf ?? []);
1067
            if ($sample) {
1068
                $parameters['sample'] = 1;
1069
            }
1070
            foreach ($parameterNames as $parameterName) {
1071
                if (isset($conf[$parameterName . '.'])) {
1072
                    $conf[$parameterName] = $this->stdWrap($conf[$parameterName], $conf[$parameterName . '.'] ?? []);
1073
                }
1074
                if (isset($conf[$parameterName]) && $conf[$parameterName]) {
1075
                    $parameters[$parameterName] = $conf[$parameterName];
1076
                }
1077
            }
1078
            $parametersEncoded = base64_encode((string)json_encode($parameters));
1079
            $hmac = GeneralUtility::hmac(implode('|', [$file->getUid(), $parametersEncoded]));
1080
            $params = '&md5=' . $hmac;
1081
            foreach (str_split($parametersEncoded, 64) as $index => $chunk) {
1082
                $params .= '&parameters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk);
1083
            }
1084
            $url = $this->getTypoScriptFrontendController()->absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params;
1085
            $directImageLink = $this->stdWrapValue('directImageLink', $conf ?? []);
1086
            if ($directImageLink) {
1087
                $imgResourceConf = [
1088
                    'file' => $imageFile,
1089
                    'file.' => $conf,
1090
                ];
1091
                $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf);
1092
                if (!$url) {
1093
                    // If no imagemagick / gm is available
1094
                    $url = $imageFile;
1095
                }
1096
            }
1097
            // Create TARGET-attribute only if the right doctype is used
1098
            $target = '';
1099
            $xhtmlDocType = $this->getTypoScriptFrontendController()->xhtmlDoctype;
1100
            if ($xhtmlDocType !== 'xhtml_strict' && $xhtmlDocType !== 'xhtml_11') {
1101
                $target = (string)$this->stdWrapValue('target', $conf ?? []);
1102
                if ($target === '') {
1103
                    $target = 'thePicture';
1104
                }
1105
            }
1106
            $a1 = '';
1107
            $a2 = '';
1108
            $conf['JSwindow'] = $this->stdWrapValue('JSwindow', $conf ?? []);
1109
            if ($conf['JSwindow']) {
1110
                $altUrl = $this->stdWrapValue('altUrl', $conf['JSwindow.'] ?? []);
1111
                if ($altUrl) {
1112
                    $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode((string)$imageFile) . $params);
0 ignored issues
show
Bug introduced by
Are you sure $altUrl of type integer|string|true can be used in concatenation? ( Ignorable by Annotation )

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

1112
                    $url = /** @scrutinizer ignore-type */ $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode((string)$imageFile) . $params);
Loading history...
1113
                }
1114
1115
                $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf);
1116
                $JSwindowExpand = $this->stdWrapValue('expand', $conf['JSwindow.'] ?? []);
1117
                $offset = GeneralUtility::intExplode(',', $JSwindowExpand . ',');
1118
                $newWindow = $this->stdWrapValue('newWindow', $conf['JSwindow.'] ?? []);
1119
                $params = [
1120
                    'width' => ($processedFile->getProperty('width') + $offset[0]),
1121
                    'height' => ($processedFile->getProperty('height') + $offset[1]),
1122
                    'status' => '0',
1123
                    'menubar' => '0',
1124
                ];
1125
                // params override existing parameters from above, or add more
1126
                $windowParams = (string)$this->stdWrapValue('params', $conf['JSwindow.'] ?? []);
1127
                $windowParams = explode(',', $windowParams);
1128
                foreach ($windowParams as $windowParam) {
1129
                    [$paramKey, $paramValue] = explode('=', $windowParam);
1130
                    if ($paramValue !== '') {
1131
                        $params[$paramKey] = $paramValue;
1132
                    } else {
1133
                        unset($params[$paramKey]);
1134
                    }
1135
                }
1136
                $paramString = '';
1137
                foreach ($params as $paramKey => $paramValue) {
1138
                    $paramString .= htmlspecialchars((string)$paramKey) . '=' . htmlspecialchars((string)$paramValue) . ',';
1139
                }
1140
1141
                $attrs = [
1142
                    'href' => (string)$url,
1143
                    'data-window-url' => $this->getTypoScriptFrontendController()->baseUrlWrap($url),
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $url of TYPO3\CMS\Frontend\Contr...ntroller::baseUrlWrap() does only seem to accept string, 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

1143
                    'data-window-url' => $this->getTypoScriptFrontendController()->baseUrlWrap(/** @scrutinizer ignore-type */ $url),
Loading history...
1144
                    'data-window-target' => $newWindow ? md5((string)$url) : 'thePicture',
1145
                    'data-window-features' => rtrim($paramString, ','),
1146
                ];
1147
                if ($target !== '') {
1148
                    $attrs['target'] = $target;
1149
                }
1150
1151
                $a1 = sprintf(
1152
                    '<a %s%s>',
1153
                    GeneralUtility::implodeAttributes($attrs, true),
1154
                    trim($this->getTypoScriptFrontendController()->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->getTypoScriptFrontendController()->config['config']['ATagParams']) : ''
1155
                );
1156
                $a2 = '</a>';
1157
                $this->addDefaultFrontendJavaScript();
1158
            } else {
1159
                $conf['linkParams.']['directImageLink'] = (bool)($conf['directImageLink'] ?? false);
1160
                $conf['linkParams.']['parameter'] = $url;
1161
                $string = $this->typoLink($string, $conf['linkParams.']);
1162
            }
1163
            if (isset($conf['stdWrap.'])) {
1164
                $string = $this->stdWrap($string, $conf['stdWrap.']);
1165
            }
1166
            $content = $a1 . $string . $a2;
1167
        }
1168
        return $content;
1169
    }
1170
1171
    /**
1172
     * Sets the SYS_LASTCHANGED timestamp if input timestamp is larger than current value.
1173
     * The SYS_LASTCHANGED timestamp can be used by various caching/indexing applications to determine if the page has new content.
1174
     * Therefore you should call this function with the last-changed timestamp of any element you display.
1175
     *
1176
     * @param int $tstamp Unix timestamp (number of seconds since 1970)
1177
     * @see TypoScriptFrontendController::setSysLastChanged()
1178
     */
1179
    public function lastChanged($tstamp)
1180
    {
1181
        $tstamp = (int)$tstamp;
1182
        $tsfe = $this->getTypoScriptFrontendController();
1183
        if ($tstamp > (int)$tsfe->register['SYS_LASTCHANGED']) {
1184
            $tsfe->register['SYS_LASTCHANGED'] = $tstamp;
1185
        }
1186
    }
1187
1188
    /**
1189
     * An abstraction method to add parameters to an A tag.
1190
     * Uses the ATagParams property, also includes the global TypoScript config.ATagParams
1191
     *
1192
     * @param array $conf TypoScript configuration properties
1193
     * @return string String containing the parameters to the A tag (if non empty, with a leading space)
1194
     * @see typolink()
1195
     */
1196
    public function getATagParams($conf)
1197
    {
1198
        $aTagParams = $this->stdWrapValue('ATagParams', $conf ?? []);
1199
        // Add the global config.ATagParams
1200
        $globalParams = trim($this->getTypoScriptFrontendController()->config['config']['ATagParams'] ?? '');
1201
        $aTagParams = ' ' . trim($globalParams . ' ' . $aTagParams);
1202
        // Extend params
1203
        $_params = [
1204
            'conf' => &$conf,
1205
            'aTagParams' => &$aTagParams,
1206
        ];
1207
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ?? [] as $className) {
1208
            $processor = GeneralUtility::makeInstance($className);
1209
            $aTagParams = $processor->process($_params, $this);
1210
        }
1211
1212
        $aTagParams = trim($aTagParams);
1213
        if (!empty($aTagParams)) {
1214
            $aTagParams = ' ' . $aTagParams;
1215
        }
1216
1217
        return $aTagParams;
1218
    }
1219
1220
    /***********************************************
1221
     *
1222
     * HTML template processing functions
1223
     *
1224
     ***********************************************/
1225
1226
    /**
1227
     * Sets the current file object during iterations over files.
1228
     *
1229
     * @param File $fileObject The file object.
1230
     */
1231
    public function setCurrentFile($fileObject)
1232
    {
1233
        $this->currentFile = $fileObject;
1234
    }
1235
1236
    /**
1237
     * Gets the current file object during iterations over files.
1238
     *
1239
     * @return File The current file object.
1240
     */
1241
    public function getCurrentFile()
1242
    {
1243
        return $this->currentFile;
1244
    }
1245
1246
    /***********************************************
1247
     *
1248
     * "stdWrap" + sub functions
1249
     *
1250
     ***********************************************/
1251
    /**
1252
     * The "stdWrap" function. This is the implementation of what is known as "stdWrap properties" in TypoScript.
1253
     * Basically "stdWrap" performs some processing of a value based on properties in the input $conf array(holding the TypoScript "stdWrap properties")
1254
     * See the link below for a complete list of properties and what they do. The order of the table with properties found in TSref (the link) follows the actual order of implementation in this function.
1255
     *
1256
     * @param string $content Input value undergoing processing in this function. Possibly substituted by other values fetched from another source.
1257
     * @param array $conf TypoScript "stdWrap properties".
1258
     * @return string The processed input value
1259
     */
1260
    public function stdWrap($content = '', $conf = [])
1261
    {
1262
        $content = (string)$content;
1263
        // If there is any hook object, activate all of the process and override functions.
1264
        // The hook interface ContentObjectStdWrapHookInterface takes care that all 4 methods exist.
1265
        if ($this->stdWrapHookObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->stdWrapHookObjects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1266
            $conf['stdWrapPreProcess'] = 1;
1267
            $conf['stdWrapOverride'] = 1;
1268
            $conf['stdWrapProcess'] = 1;
1269
            $conf['stdWrapPostProcess'] = 1;
1270
        }
1271
1272
        if (!is_array($conf) || !$conf) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
1273
            return $content;
1274
        }
1275
1276
        // Cache handling
1277
        if (isset($conf['cache.']) && is_array($conf['cache.'])) {
1278
            $conf['cache.']['key'] = $this->stdWrapValue('key', $conf['cache.'] ?? []);
1279
            $conf['cache.']['tags'] = $this->stdWrapValue('tags', $conf['cache.'] ?? []);
1280
            $conf['cache.']['lifetime'] = $this->stdWrapValue('lifetime', $conf['cache.'] ?? []);
1281
            $conf['cacheRead'] = 1;
1282
            $conf['cacheStore'] = 1;
1283
        }
1284
        // The configuration is sorted and filtered by intersection with the defined stdWrapOrder.
1285
        $sortedConf = array_keys(array_intersect_key($this->stdWrapOrder, $conf));
1286
        // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions
1287
        $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap';
1288
        // Additional Array to check whether a function has already been executed
1289
        $isExecuted = [];
1290
        // Additional switch to make sure 'required', 'if' and 'fieldRequired'
1291
        // will still stop rendering immediately in case they return FALSE
1292
        $this->stdWrapRecursionLevel++;
1293
        $this->stopRendering[$this->stdWrapRecursionLevel] = false;
1294
        // execute each function in the predefined order
1295
        foreach ($sortedConf as $stdWrapName) {
1296
            // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped
1297
            if ((!isset($isExecuted[$stdWrapName]) || !$isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapRecursionLevel]) {
1298
                $functionName = rtrim($stdWrapName, '.');
1299
                $functionProperties = $functionName . '.';
1300
                $functionType = $this->stdWrapOrder[$functionName] ?? '';
1301
                // If there is any code on the next level, check if it contains "official" stdWrap functions
1302
                // if yes, execute them first - will make each function stdWrap aware
1303
                // so additional stdWrap calls within the functions can be removed, since the result will be the same
1304
                if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) {
1305
                    if (array_intersect_key($this->stdWrapOrder, $conf[$functionProperties])) {
1306
                        // Check if there's already content available before processing
1307
                        // any ifEmpty or ifBlank stdWrap properties
1308
                        if (($functionName === 'ifBlank' && $content !== '') ||
1309
                            ($functionName === 'ifEmpty' && trim($content) !== '')) {
1310
                            continue;
1311
                        }
1312
1313
                        $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties] ?? []);
1314
                    }
1315
                }
1316
                // Check if key is still containing something, since it might have been changed by next level stdWrap before
1317
                if ((isset($conf[$functionName]) || $conf[$functionProperties])
1318
                    && ($functionType !== 'boolean' || $conf[$functionName])
1319
                ) {
1320
                    // Get just that part of $conf that is needed for the particular function
1321
                    $singleConf = [
1322
                        $functionName => $conf[$functionName] ?? null,
1323
                        $functionProperties => $conf[$functionProperties] ?? null,
1324
                    ];
1325
                    // Hand over the whole $conf array to the stdWrapHookObjects
1326
                    if ($functionType === 'hook') {
1327
                        $singleConf = $conf;
1328
                    }
1329
                    // Add both keys - with and without the dot - to the set of executed functions
1330
                    $isExecuted[$functionName] = true;
1331
                    $isExecuted[$functionProperties] = true;
1332
                    // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array
1333
                    $functionName = 'stdWrap_' . $functionName;
1334
                    $content = $this->{$functionName}($content, $singleConf);
1335
                } elseif ($functionType === 'boolean' && !$conf[$functionName]) {
1336
                    $isExecuted[$functionName] = true;
1337
                    $isExecuted[$functionProperties] = true;
1338
                }
1339
            }
1340
        }
1341
        unset($this->stopRendering[$this->stdWrapRecursionLevel]);
1342
        $this->stdWrapRecursionLevel--;
1343
1344
        return $content;
1345
    }
1346
1347
    /**
1348
     * Gets a configuration value by passing them through stdWrap first and taking a default value if stdWrap doesn't yield a result.
1349
     *
1350
     * @param string $key The config variable key (from TS array).
1351
     * @param array $config The TypoScript array.
1352
     * @param string|int|bool|null $defaultValue Optional default value.
1353
     * @return string|int|bool|null Value of the config variable
1354
     */
1355
    public function stdWrapValue($key, array $config, $defaultValue = '')
1356
    {
1357
        if (isset($config[$key])) {
1358
            if (!isset($config[$key . '.'])) {
1359
                return $config[$key];
1360
            }
1361
        } elseif (isset($config[$key . '.'])) {
1362
            $config[$key] = '';
1363
        } else {
1364
            return $defaultValue;
1365
        }
1366
        $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']);
1367
        // The string "0" should be returned.
1368
        return $stdWrapped !== '' ? $stdWrapped : $defaultValue;
1369
    }
1370
1371
    /**
1372
     * stdWrap pre process hook
1373
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1374
     * this hook will execute functions before any other stdWrap function can modify anything
1375
     *
1376
     * @param string $content Input value undergoing processing in these functions.
1377
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1378
     * @return string The processed input value
1379
     */
1380
    public function stdWrap_stdWrapPreProcess($content = '', $conf = [])
1381
    {
1382
        foreach ($this->stdWrapHookObjects as $hookObject) {
1383
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1384
            $content = $hookObject->stdWrapPreProcess($content, $conf, $this);
1385
        }
1386
        return $content;
1387
    }
1388
1389
    /**
1390
     * Check if content was cached before (depending on the given cache key)
1391
     *
1392
     * @param string $content Input value undergoing processing in these functions.
1393
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1394
     * @return string The processed input value
1395
     */
1396
    public function stdWrap_cacheRead($content = '', $conf = [])
1397
    {
1398
        if (!isset($conf['cache.'])) {
1399
            return $content;
1400
        }
1401
        $result = $this->getFromCache($conf['cache.']);
1402
        return $result === false ? $content : $result;
1403
    }
1404
1405
    /**
1406
     * Add tags to page cache (comma-separated list)
1407
     *
1408
     * @param string $content Input value undergoing processing in these functions.
1409
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1410
     * @return string The processed input value
1411
     */
1412
    public function stdWrap_addPageCacheTags($content = '', $conf = [])
1413
    {
1414
        $tags = (string)$this->stdWrapValue('addPageCacheTags', $conf ?? []);
1415
        if (!empty($tags)) {
1416
            $cacheTags = GeneralUtility::trimExplode(',', $tags, true);
1417
            $this->getTypoScriptFrontendController()->addCacheTags($cacheTags);
1418
        }
1419
        return $content;
1420
    }
1421
1422
    /**
1423
     * setContentToCurrent
1424
     * actually it just does the contrary: Sets the value of 'current' based on current content
1425
     *
1426
     * @param string $content Input value undergoing processing in this function.
1427
     * @return string The processed input value
1428
     */
1429
    public function stdWrap_setContentToCurrent($content = '')
1430
    {
1431
        $this->data[$this->currentValKey] = $content;
1432
        return $content;
1433
    }
1434
1435
    /**
1436
     * setCurrent
1437
     * Sets the value of 'current' based on the outcome of stdWrap operations
1438
     *
1439
     * @param string $content Input value undergoing processing in this function.
1440
     * @param array $conf stdWrap properties for setCurrent.
1441
     * @return string The processed input value
1442
     */
1443
    public function stdWrap_setCurrent($content = '', $conf = [])
1444
    {
1445
        $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null;
1446
        return $content;
1447
    }
1448
1449
    /**
1450
     * lang
1451
     * Translates content based on the language currently used by the FE
1452
     *
1453
     * @param string $content Input value undergoing processing in this function.
1454
     * @param array $conf stdWrap properties for lang.
1455
     * @return string The processed input value
1456
     */
1457
    public function stdWrap_lang($content = '', $conf = [])
1458
    {
1459
        $currentLanguageCode = $this->getTypoScriptFrontendController()->getLanguage()->getTypo3Language();
1460
        if (!$currentLanguageCode) {
1461
            return $content;
1462
        }
1463
        if (isset($conf['lang.'][$currentLanguageCode])) {
1464
            $content = $conf['lang.'][$currentLanguageCode];
1465
        } else {
1466
            // Check language dependencies
1467
            $locales = GeneralUtility::makeInstance(Locales::class);
1468
            foreach ($locales->getLocaleDependencies($currentLanguageCode) as $languageCode) {
1469
                if (isset($conf['lang.'][$languageCode])) {
1470
                    $content = $conf['lang.'][$languageCode];
1471
                    break;
1472
                }
1473
            }
1474
        }
1475
        return $content;
1476
    }
1477
1478
    /**
1479
     * Gets content from different sources based on getText functions.
1480
     *
1481
     * @param string $content Input value undergoing processing in this function.
1482
     * @param array $conf stdWrap properties for data.
1483
     * @return string The processed input value
1484
     */
1485
    public function stdWrap_data($content = '', $conf = [])
1486
    {
1487
        // @deprecated since v11, will be removed in v12. Drop together with property $this->alternativeData.
1488
        // @todo v12 version: "return $this->getData($conf['data'], $this->data);"
1489
        $content = $this->getData($conf['data'], is_array($this->alternativeData) ? $this->alternativeData : $this->data);
0 ignored issues
show
introduced by
The condition is_array($this->alternativeData) is always false.
Loading history...
Deprecated Code introduced by
The property TYPO3\CMS\Frontend\Conte...derer::$alternativeData has been deprecated: since v11, will be removed in v12. Drop together with usages in this class. ( Ignorable by Annotation )

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

1489
        $content = $this->getData($conf['data'], is_array(/** @scrutinizer ignore-deprecated */ $this->alternativeData) ? $this->alternativeData : $this->data);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1490
        $this->alternativeData = '';
0 ignored issues
show
Deprecated Code introduced by
The property TYPO3\CMS\Frontend\Conte...derer::$alternativeData has been deprecated: since v11, will be removed in v12. Drop together with usages in this class. ( Ignorable by Annotation )

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

1490
        /** @scrutinizer ignore-deprecated */ $this->alternativeData = '';

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1491
        return $content;
1492
    }
1493
1494
    /**
1495
     * field
1496
     * Gets content from a DB field
1497
     *
1498
     * @param string $content Input value undergoing processing in this function.
1499
     * @param array $conf stdWrap properties for field.
1500
     * @return string The processed input value
1501
     */
1502
    public function stdWrap_field($content = '', $conf = [])
1503
    {
1504
        return $this->getFieldVal($conf['field']);
1505
    }
1506
1507
    /**
1508
     * current
1509
     * Gets content that has been previously set as 'current'
1510
     * Can be set via setContentToCurrent or setCurrent or will be set automatically i.e. inside the split function
1511
     *
1512
     * @param string $content Input value undergoing processing in this function.
1513
     * @param array $conf stdWrap properties for current.
1514
     * @return string The processed input value
1515
     */
1516
    public function stdWrap_current($content = '', $conf = [])
1517
    {
1518
        return $this->data[$this->currentValKey];
1519
    }
1520
1521
    /**
1522
     * cObject
1523
     * Will replace the content with the value of an official TypoScript cObject
1524
     * like TEXT, COA, HMENU
1525
     *
1526
     * @param string $content Input value undergoing processing in this function.
1527
     * @param array $conf stdWrap properties for cObject.
1528
     * @return string The processed input value
1529
     */
1530
    public function stdWrap_cObject($content = '', $conf = [])
1531
    {
1532
        return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject');
1533
    }
1534
1535
    /**
1536
     * numRows
1537
     * Counts the number of returned records of a DB operation
1538
     * makes use of select internally
1539
     *
1540
     * @param string $content Input value undergoing processing in this function.
1541
     * @param array $conf stdWrap properties for numRows.
1542
     * @return string The processed input value
1543
     */
1544
    public function stdWrap_numRows($content = '', $conf = [])
1545
    {
1546
        return $this->numRows($conf['numRows.']);
1547
    }
1548
1549
    /**
1550
     * preUserFunc
1551
     * Will execute a user public function before the content will be modified by any other stdWrap function
1552
     *
1553
     * @param string $content Input value undergoing processing in this function.
1554
     * @param array $conf stdWrap properties for preUserFunc.
1555
     * @return string The processed input value
1556
     */
1557
    public function stdWrap_preUserFunc($content = '', $conf = [])
1558
    {
1559
        return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'] ?? [], $content);
1560
    }
1561
1562
    /**
1563
     * stdWrap override hook
1564
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1565
     * this hook will execute functions on existing content but still before the content gets modified or replaced
1566
     *
1567
     * @param string $content Input value undergoing processing in these functions.
1568
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1569
     * @return string The processed input value
1570
     */
1571
    public function stdWrap_stdWrapOverride($content = '', $conf = [])
1572
    {
1573
        foreach ($this->stdWrapHookObjects as $hookObject) {
1574
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1575
            $content = $hookObject->stdWrapOverride($content, $conf, $this);
1576
        }
1577
        return $content;
1578
    }
1579
1580
    /**
1581
     * override
1582
     * Will override the current value of content with its own value'
1583
     *
1584
     * @param string $content Input value undergoing processing in this function.
1585
     * @param array $conf stdWrap properties for override.
1586
     * @return string The processed input value
1587
     */
1588
    public function stdWrap_override($content = '', $conf = [])
1589
    {
1590
        if (trim($conf['override'] ?? false)) {
0 ignored issues
show
Bug introduced by
It seems like $conf['override'] ?? false can also be of type false; however, parameter $string of trim() does only seem to accept string, 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

1590
        if (trim(/** @scrutinizer ignore-type */ $conf['override'] ?? false)) {
Loading history...
1591
            $content = $conf['override'];
1592
        }
1593
        return $content;
1594
    }
1595
1596
    /**
1597
     * preIfEmptyListNum
1598
     * Gets a value off a CSV list before the following ifEmpty check
1599
     * Makes sure that the result of ifEmpty will be TRUE in case the CSV does not contain a value at the position given by preIfEmptyListNum
1600
     *
1601
     * @param string $content Input value undergoing processing in this function.
1602
     * @param array $conf stdWrap properties for preIfEmptyListNum.
1603
     * @return string The processed input value
1604
     */
1605
    public function stdWrap_preIfEmptyListNum($content = '', $conf = [])
1606
    {
1607
        return $this->listNum($content, $conf['preIfEmptyListNum'] ?? null, $conf['preIfEmptyListNum.']['splitChar'] ?? null);
1608
    }
1609
1610
    /**
1611
     * ifNull
1612
     * Will set content to a replacement value in case the value of content is NULL
1613
     *
1614
     * @param string|null $content Input value undergoing processing in this function.
1615
     * @param array $conf stdWrap properties for ifNull.
1616
     * @return string The processed input value
1617
     */
1618
    public function stdWrap_ifNull($content = '', $conf = [])
1619
    {
1620
        return $content ?? $conf['ifNull'];
1621
    }
1622
1623
    /**
1624
     * ifEmpty
1625
     * Will set content to a replacement value in case the trimmed value of content returns FALSE
1626
     * 0 (zero) will be replaced as well
1627
     *
1628
     * @param string $content Input value undergoing processing in this function.
1629
     * @param array $conf stdWrap properties for ifEmpty.
1630
     * @return string The processed input value
1631
     */
1632
    public function stdWrap_ifEmpty($content = '', $conf = [])
1633
    {
1634
        if (!trim((string)$content)) {
1635
            $content = $conf['ifEmpty'];
1636
        }
1637
        return $content;
1638
    }
1639
1640
    /**
1641
     * ifBlank
1642
     * Will set content to a replacement value in case the trimmed value of content has no length
1643
     * 0 (zero) will not be replaced
1644
     *
1645
     * @param string $content Input value undergoing processing in this function.
1646
     * @param array $conf stdWrap properties for ifBlank.
1647
     * @return string The processed input value
1648
     */
1649
    public function stdWrap_ifBlank($content = '', $conf = [])
1650
    {
1651
        if (trim((string)$content) === '') {
1652
            $content = $conf['ifBlank'];
1653
        }
1654
        return $content;
1655
    }
1656
1657
    /**
1658
     * listNum
1659
     * Gets a value off a CSV list after ifEmpty check
1660
     * Might return an empty value in case the CSV does not contain a value at the position given by listNum
1661
     * Use preIfEmptyListNum to avoid that behaviour
1662
     *
1663
     * @param string $content Input value undergoing processing in this function.
1664
     * @param array $conf stdWrap properties for listNum.
1665
     * @return string The processed input value
1666
     */
1667
    public function stdWrap_listNum($content = '', $conf = [])
1668
    {
1669
        return $this->listNum($content, $conf['listNum'] ?? null, $conf['listNum.']['splitChar'] ?? null);
1670
    }
1671
1672
    /**
1673
     * trim
1674
     * Cuts off any whitespace at the beginning and the end of the content
1675
     *
1676
     * @param string $content Input value undergoing processing in this function.
1677
     * @return string The processed input value
1678
     */
1679
    public function stdWrap_trim($content = '')
1680
    {
1681
        return trim((string)$content);
1682
    }
1683
1684
    /**
1685
     * strPad
1686
     * Will return a string padded left/right/on both sides, based on configuration given as stdWrap properties
1687
     *
1688
     * @param string $content Input value undergoing processing in this function.
1689
     * @param array $conf stdWrap properties for strPad.
1690
     * @return string The processed input value
1691
     */
1692
    public function stdWrap_strPad($content = '', $conf = [])
1693
    {
1694
        // Must specify a length in conf for this to make sense
1695
        $length = (int)$this->stdWrapValue('length', $conf['strPad.'] ?? [], 0);
1696
        // Padding with space is PHP-default
1697
        $padWith = (string)$this->stdWrapValue('padWith', $conf['strPad.'] ?? [], ' ');
1698
        // Padding on the right side is PHP-default
1699
        $padType = STR_PAD_RIGHT;
1700
1701
        if (!empty($conf['strPad.']['type'])) {
1702
            $type = (string)$this->stdWrapValue('type', $conf['strPad.'] ?? []);
1703
            if (strtolower($type) === 'left') {
1704
                $padType = STR_PAD_LEFT;
1705
            } elseif (strtolower($type) === 'both') {
1706
                $padType = STR_PAD_BOTH;
1707
            }
1708
        }
1709
        return StringUtility::multibyteStringPad($content, $length, $padWith, $padType);
1710
    }
1711
1712
    /**
1713
     * stdWrap
1714
     * A recursive call of the stdWrap function set
1715
     * This enables the user to execute stdWrap functions in another than the predefined order
1716
     * It modifies the content, not the property
1717
     * while the new feature of chained stdWrap functions modifies the property and not the content
1718
     *
1719
     * @param string $content Input value undergoing processing in this function.
1720
     * @param array $conf stdWrap properties for stdWrap.
1721
     * @return string The processed input value
1722
     */
1723
    public function stdWrap_stdWrap($content = '', $conf = [])
1724
    {
1725
        return $this->stdWrap($content, $conf['stdWrap.']);
1726
    }
1727
1728
    /**
1729
     * stdWrap process hook
1730
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1731
     * this hook executes functions directly after the recursive stdWrap function call but still before the content gets modified
1732
     *
1733
     * @param string $content Input value undergoing processing in these functions.
1734
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1735
     * @return string The processed input value
1736
     */
1737
    public function stdWrap_stdWrapProcess($content = '', $conf = [])
1738
    {
1739
        foreach ($this->stdWrapHookObjects as $hookObject) {
1740
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1741
            $content = $hookObject->stdWrapProcess($content, $conf, $this);
1742
        }
1743
        return $content;
1744
    }
1745
1746
    /**
1747
     * required
1748
     * Will immediately stop rendering and return an empty value
1749
     * when there is no content at this point
1750
     *
1751
     * @param string $content Input value undergoing processing in this function.
1752
     * @return string The processed input value
1753
     */
1754
    public function stdWrap_required($content = '')
1755
    {
1756
        if ((string)$content === '') {
1757
            $content = '';
1758
            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1759
        }
1760
        return $content;
1761
    }
1762
1763
    /**
1764
     * if
1765
     * Will immediately stop rendering and return an empty value
1766
     * when the result of the checks returns FALSE
1767
     *
1768
     * @param string $content Input value undergoing processing in this function.
1769
     * @param array $conf stdWrap properties for if.
1770
     * @return string The processed input value
1771
     */
1772
    public function stdWrap_if($content = '', $conf = [])
1773
    {
1774
        if (empty($conf['if.']) || $this->checkIf($conf['if.'])) {
1775
            return $content;
1776
        }
1777
        $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1778
        return '';
1779
    }
1780
1781
    /**
1782
     * fieldRequired
1783
     * Will immediately stop rendering and return an empty value
1784
     * when there is no content in the field given by fieldRequired
1785
     *
1786
     * @param string $content Input value undergoing processing in this function.
1787
     * @param array $conf stdWrap properties for fieldRequired.
1788
     * @return string The processed input value
1789
     */
1790
    public function stdWrap_fieldRequired($content = '', $conf = [])
1791
    {
1792
        if (!trim($this->data[$conf['fieldRequired'] ?? null] ?? '')) {
1793
            $content = '';
1794
            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1795
        }
1796
        return $content;
1797
    }
1798
1799
    /**
1800
     * stdWrap csConv: Converts the input to UTF-8
1801
     *
1802
     * The character set of the input must be specified. Returns the input if
1803
     * matters go wrong, for example if an invalid character set is given.
1804
     *
1805
     * @param string $content The string to convert.
1806
     * @param array $conf stdWrap properties for csConv.
1807
     * @return string The processed input.
1808
     */
1809
    public function stdWrap_csConv($content = '', $conf = [])
1810
    {
1811
        if (!empty($conf['csConv'])) {
1812
            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv'])));
1813
            return $output !== false && $output !== '' ? $output : $content;
1814
        }
1815
        return $content;
1816
    }
1817
1818
    /**
1819
     * parseFunc
1820
     * Will parse the content based on functions given as stdWrap properties
1821
     * Heavily used together with RTE based content
1822
     *
1823
     * @param string $content Input value undergoing processing in this function.
1824
     * @param array $conf stdWrap properties for parseFunc.
1825
     * @return string The processed input value
1826
     */
1827
    public function stdWrap_parseFunc($content = '', $conf = [])
1828
    {
1829
        return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']);
1830
    }
1831
1832
    /**
1833
     * HTMLparser
1834
     * Will parse HTML content based on functions given as stdWrap properties
1835
     * Heavily used together with RTE based content
1836
     *
1837
     * @param string $content Input value undergoing processing in this function.
1838
     * @param array $conf stdWrap properties for HTMLparser.
1839
     * @return string The processed input value
1840
     */
1841
    public function stdWrap_HTMLparser($content = '', $conf = [])
1842
    {
1843
        if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) {
1844
            $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']);
1845
        }
1846
        return $content;
1847
    }
1848
1849
    /**
1850
     * split
1851
     * Will split the content by a given token and treat the results separately
1852
     * Automatically fills 'current' with a single result
1853
     *
1854
     * @param string $content Input value undergoing processing in this function.
1855
     * @param array $conf stdWrap properties for split.
1856
     * @return string The processed input value
1857
     */
1858
    public function stdWrap_split($content = '', $conf = [])
1859
    {
1860
        return $this->splitObj($content, $conf['split.']);
1861
    }
1862
1863
    /**
1864
     * replacement
1865
     * Will execute replacements on the content (optionally with preg-regex)
1866
     *
1867
     * @param string $content Input value undergoing processing in this function.
1868
     * @param array $conf stdWrap properties for replacement.
1869
     * @return string The processed input value
1870
     */
1871
    public function stdWrap_replacement($content = '', $conf = [])
1872
    {
1873
        return $this->replacement($content, $conf['replacement.']);
1874
    }
1875
1876
    /**
1877
     * prioriCalc
1878
     * Will use the content as a mathematical term and calculate the result
1879
     * Can be set to 1 to just get a calculated value or 'intval' to get the integer of the result
1880
     *
1881
     * @param string $content Input value undergoing processing in this function.
1882
     * @param array $conf stdWrap properties for prioriCalc.
1883
     * @return string The processed input value
1884
     */
1885
    public function stdWrap_prioriCalc($content = '', $conf = [])
1886
    {
1887
        $content = MathUtility::calculateWithParentheses($content);
1888
        if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') {
1889
            $content = (int)$content;
1890
        }
1891
        return $content;
1892
    }
1893
1894
    /**
1895
     * char
1896
     * Returns a one-character string containing the character specified by ascii code.
1897
     *
1898
     * Reliable results only for character codes in the integer range 0 - 127.
1899
     *
1900
     * @see https://php.net/manual/en/function.chr.php
1901
     * @param string $content Input value undergoing processing in this function.
1902
     * @param array $conf stdWrap properties for char.
1903
     * @return string The processed input value
1904
     */
1905
    public function stdWrap_char($content = '', $conf = [])
1906
    {
1907
        return chr((int)$conf['char']);
1908
    }
1909
1910
    /**
1911
     * intval
1912
     * Will return an integer value of the current content
1913
     *
1914
     * @param string $content Input value undergoing processing in this function.
1915
     * @return string The processed input value
1916
     */
1917
    public function stdWrap_intval($content = '')
1918
    {
1919
        return (int)$content;
1920
    }
1921
1922
    /**
1923
     * Will return a hashed value of the current content
1924
     *
1925
     * @param string $content Input value undergoing processing in this function.
1926
     * @param array $conf stdWrap properties for hash.
1927
     * @return string The processed input value
1928
     * @link https://php.net/manual/de/function.hash-algos.php for a list of supported hash algorithms
1929
     */
1930
    public function stdWrap_hash($content = '', array $conf = [])
1931
    {
1932
        $algorithm = (string)$this->stdWrapValue('hash', $conf ?? []);
1933
        if (function_exists('hash') && in_array($algorithm, hash_algos())) {
1934
            return hash($algorithm, $content);
1935
        }
1936
        // Non-existing hashing algorithm
1937
        return '';
1938
    }
1939
1940
    /**
1941
     * stdWrap_round will return a rounded number with ceil(), floor() or round(), defaults to round()
1942
     * Only the english number format is supported . (dot) as decimal point
1943
     *
1944
     * @param string $content Input value undergoing processing in this function.
1945
     * @param array $conf stdWrap properties for round.
1946
     * @return string The processed input value
1947
     */
1948
    public function stdWrap_round($content = '', $conf = [])
1949
    {
1950
        return $this->round($content, $conf['round.']);
1951
    }
1952
1953
    /**
1954
     * numberFormat
1955
     * Will return a formatted number based on configuration given as stdWrap properties
1956
     *
1957
     * @param string $content Input value undergoing processing in this function.
1958
     * @param array $conf stdWrap properties for numberFormat.
1959
     * @return string The processed input value
1960
     */
1961
    public function stdWrap_numberFormat($content = '', $conf = [])
1962
    {
1963
        return $this->numberFormat((float)$content, $conf['numberFormat.'] ?? []);
1964
    }
1965
1966
    /**
1967
     * expandList
1968
     * Will return a formatted number based on configuration given as stdWrap properties
1969
     *
1970
     * @param string $content Input value undergoing processing in this function.
1971
     * @return string The processed input value
1972
     */
1973
    public function stdWrap_expandList($content = '')
1974
    {
1975
        return GeneralUtility::expandList($content);
1976
    }
1977
1978
    /**
1979
     * date
1980
     * Will return a formatted date based on configuration given according to PHP date/gmdate properties
1981
     * Will return gmdate when the property GMT returns TRUE
1982
     *
1983
     * @param string $content Input value undergoing processing in this function.
1984
     * @param array $conf stdWrap properties for date.
1985
     * @return string The processed input value
1986
     */
1987
    public function stdWrap_date($content = '', $conf = [])
1988
    {
1989
        // Check for zero length string to mimic default case of date/gmdate.
1990
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
1991
        $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
0 ignored issues
show
Bug introduced by
It seems like $conf['date'] ?? null can also be of type null; however, parameter $format of gmdate() does only seem to accept string, 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

1991
        $content = !empty($conf['date.']['GMT']) ? gmdate(/** @scrutinizer ignore-type */ $conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
Loading history...
Bug introduced by
It seems like $conf['date'] ?? null can also be of type null; however, parameter $format of date() does only seem to accept string, 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

1991
        $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date(/** @scrutinizer ignore-type */ $conf['date'] ?? null, $content);
Loading history...
1992
        return $content;
1993
    }
1994
1995
    /**
1996
     * strftime
1997
     * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties
1998
     * Will return gmstrftime when the property GMT returns TRUE
1999
     *
2000
     * @param string $content Input value undergoing processing in this function.
2001
     * @param array $conf stdWrap properties for strftime.
2002
     * @return string The processed input value
2003
     * @todo @todo Replace deprecated strftime/gmstrftime and whole method in php 8.1. Suppress warning in v11.
2004
     */
2005
    public function stdWrap_strftime($content = '', $conf = [])
2006
    {
2007
        // Check for zero length string to mimic default case of strtime/gmstrftime
2008
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
2009
        $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT'])
2010
            ? @gmstrftime($conf['strftime'] ?? null, $content)
0 ignored issues
show
Bug introduced by
It seems like $conf['strftime'] ?? null can also be of type null; however, parameter $format of gmstrftime() does only seem to accept string, 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

2010
            ? @gmstrftime(/** @scrutinizer ignore-type */ $conf['strftime'] ?? null, $content)
Loading history...
2011
            : @strftime($conf['strftime'] ?? null, $content);
0 ignored issues
show
Bug introduced by
It seems like $conf['strftime'] ?? null can also be of type null; however, parameter $format of strftime() does only seem to accept string, 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

2011
            : @strftime(/** @scrutinizer ignore-type */ $conf['strftime'] ?? null, $content);
Loading history...
2012
        if (!empty($conf['strftime.']['charset'])) {
2013
            $output = mb_convert_encoding((string)$content, 'utf-8', trim(strtolower($conf['strftime.']['charset'])));
2014
            return $output ?: $content;
2015
        }
2016
        return $content;
2017
    }
2018
2019
    /**
2020
     * strtotime
2021
     * Will return a timestamp based on configuration given according to PHP strtotime
2022
     *
2023
     * @param string $content Input value undergoing processing in this function.
2024
     * @param array $conf stdWrap properties for strtotime.
2025
     * @return string The processed input value
2026
     */
2027
    public function stdWrap_strtotime($content = '', $conf = [])
2028
    {
2029
        if ($conf['strtotime'] !== '1') {
2030
            $content .= ' ' . $conf['strtotime'];
2031
        }
2032
        return strtotime($content, $GLOBALS['EXEC_TIME']);
2033
    }
2034
2035
    /**
2036
     * age
2037
     * Will return the age of a given timestamp based on configuration given by stdWrap properties
2038
     *
2039
     * @param string $content Input value undergoing processing in this function.
2040
     * @param array $conf stdWrap properties for age.
2041
     * @return string The processed input value
2042
     */
2043
    public function stdWrap_age($content = '', $conf = [])
2044
    {
2045
        return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null);
2046
    }
2047
2048
    /**
2049
     * case
2050
     * Will transform the content to be upper or lower case only
2051
     * Leaves HTML tags untouched
2052
     *
2053
     * @param string $content Input value undergoing processing in this function.
2054
     * @param array $conf stdWrap properties for case.
2055
     * @return string The processed input value
2056
     */
2057
    public function stdWrap_case($content = '', $conf = [])
2058
    {
2059
        return $this->HTMLcaseshift($content, $conf['case']);
2060
    }
2061
2062
    /**
2063
     * bytes
2064
     * Will return the size of a given number in Bytes	 *
2065
     *
2066
     * @param string $content Input value undergoing processing in this function.
2067
     * @param array $conf stdWrap properties for bytes.
2068
     * @return string The processed input value
2069
     */
2070
    public function stdWrap_bytes($content = '', $conf = [])
2071
    {
2072
        return GeneralUtility::formatSize((int)$content, $conf['bytes.']['labels'], $conf['bytes.']['base']);
2073
    }
2074
2075
    /**
2076
     * substring
2077
     * Will return a substring based on position information given by stdWrap properties
2078
     *
2079
     * @param string $content Input value undergoing processing in this function.
2080
     * @param array $conf stdWrap properties for substring.
2081
     * @return string The processed input value
2082
     */
2083
    public function stdWrap_substring($content = '', $conf = [])
2084
    {
2085
        return $this->substring($content, $conf['substring']);
2086
    }
2087
2088
    /**
2089
     * cropHTML
2090
     * Crops content to a given size while leaving HTML tags untouched
2091
     *
2092
     * @param string $content Input value undergoing processing in this function.
2093
     * @param array $conf stdWrap properties for cropHTML.
2094
     * @return string The processed input value
2095
     */
2096
    public function stdWrap_cropHTML($content = '', $conf = [])
2097
    {
2098
        return $this->cropHTML($content, $conf['cropHTML'] ?? '');
2099
    }
2100
2101
    /**
2102
     * stripHtml
2103
     * Completely removes HTML tags from content
2104
     *
2105
     * @param string $content Input value undergoing processing in this function.
2106
     * @return string The processed input value
2107
     */
2108
    public function stdWrap_stripHtml($content = '')
2109
    {
2110
        return strip_tags($content);
2111
    }
2112
2113
    /**
2114
     * crop
2115
     * Crops content to a given size without caring about HTML tags
2116
     *
2117
     * @param string $content Input value undergoing processing in this function.
2118
     * @param array $conf stdWrap properties for crop.
2119
     * @return string The processed input value
2120
     */
2121
    public function stdWrap_crop($content = '', $conf = [])
2122
    {
2123
        return $this->crop($content, $conf['crop']);
2124
    }
2125
2126
    /**
2127
     * rawUrlEncode
2128
     * Encodes content to be used within URLs
2129
     *
2130
     * @param string $content Input value undergoing processing in this function.
2131
     * @return string The processed input value
2132
     */
2133
    public function stdWrap_rawUrlEncode($content = '')
2134
    {
2135
        return rawurlencode($content);
2136
    }
2137
2138
    /**
2139
     * htmlSpecialChars
2140
     * Transforms HTML tags to readable text by replacing special characters with their HTML entity
2141
     * When preserveEntities returns TRUE, existing entities will be left untouched
2142
     *
2143
     * @param string $content Input value undergoing processing in this function.
2144
     * @param array $conf stdWrap properties for htmlSpecialChars.
2145
     * @return string The processed input value
2146
     */
2147
    public function stdWrap_htmlSpecialChars($content = '', $conf = [])
2148
    {
2149
        if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) {
2150
            $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false);
2151
        } else {
2152
            $content = htmlspecialchars($content);
2153
        }
2154
        return $content;
2155
    }
2156
2157
    /**
2158
     * encodeForJavaScriptValue
2159
     * Escapes content to be used inside JavaScript strings. Single quotes are added around the value.
2160
     *
2161
     * @param string $content Input value undergoing processing in this function
2162
     * @return string The processed input value
2163
     */
2164
    public function stdWrap_encodeForJavaScriptValue($content = '')
2165
    {
2166
        return GeneralUtility::quoteJSvalue($content);
2167
    }
2168
2169
    /**
2170
     * doubleBrTag
2171
     * Searches for double line breaks and replaces them with the given value
2172
     *
2173
     * @param string $content Input value undergoing processing in this function.
2174
     * @param array $conf stdWrap properties for doubleBrTag.
2175
     * @return string The processed input value
2176
     */
2177
    public function stdWrap_doubleBrTag($content = '', $conf = [])
2178
    {
2179
        return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? '', $content);
2180
    }
2181
2182
    /**
2183
     * br
2184
     * Searches for single line breaks and replaces them with a <br />/<br> tag
2185
     * according to the doctype
2186
     *
2187
     * @param string $content Input value undergoing processing in this function.
2188
     * @return string The processed input value
2189
     */
2190
    public function stdWrap_br($content = '')
2191
    {
2192
        return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype));
2193
    }
2194
2195
    /**
2196
     * brTag
2197
     * Searches for single line feeds and replaces them with the given value
2198
     *
2199
     * @param string $content Input value undergoing processing in this function.
2200
     * @param array $conf stdWrap properties for brTag.
2201
     * @return string The processed input value
2202
     */
2203
    public function stdWrap_brTag($content = '', $conf = [])
2204
    {
2205
        return str_replace(LF, (string)($conf['brTag'] ?? ''), $content);
2206
    }
2207
2208
    /**
2209
     * encapsLines
2210
     * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet
2211
     * and wrapping them with values given by stdWrap properties
2212
     *
2213
     * @param string $content Input value undergoing processing in this function.
2214
     * @param array $conf stdWrap properties for erncapsLines.
2215
     * @return string The processed input value
2216
     */
2217
    public function stdWrap_encapsLines($content = '', $conf = [])
2218
    {
2219
        return $this->encaps_lineSplit($content, $conf['encapsLines.']);
2220
    }
2221
2222
    /**
2223
     * keywords
2224
     * Transforms content into a CSV list to be used i.e. as keywords within a meta tag
2225
     *
2226
     * @param string $content Input value undergoing processing in this function.
2227
     * @return string The processed input value
2228
     */
2229
    public function stdWrap_keywords($content = '')
2230
    {
2231
        return $this->keywords($content);
2232
    }
2233
2234
    /**
2235
     * innerWrap
2236
     * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2237
     * See wrap
2238
     *
2239
     * @param string $content Input value undergoing processing in this function.
2240
     * @param array $conf stdWrap properties for innerWrap.
2241
     * @return string The processed input value
2242
     */
2243
    public function stdWrap_innerWrap($content = '', $conf = [])
2244
    {
2245
        return $this->wrap($content, $conf['innerWrap'] ?? null);
2246
    }
2247
2248
    /**
2249
     * innerWrap2
2250
     * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2251
     * See wrap
2252
     *
2253
     * @param string $content Input value undergoing processing in this function.
2254
     * @param array $conf stdWrap properties for innerWrap2.
2255
     * @return string The processed input value
2256
     */
2257
    public function stdWrap_innerWrap2($content = '', $conf = [])
2258
    {
2259
        return $this->wrap($content, $conf['innerWrap2'] ?? null);
2260
    }
2261
2262
    /**
2263
     * preCObject
2264
     * A content object that is prepended to the current content but between the innerWraps and the rest of the wraps
2265
     *
2266
     * @param string $content Input value undergoing processing in this function.
2267
     * @param array $conf stdWrap properties for preCObject.
2268
     * @return string The processed input value
2269
     */
2270
    public function stdWrap_preCObject($content = '', $conf = [])
2271
    {
2272
        return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content;
2273
    }
2274
2275
    /**
2276
     * postCObject
2277
     * A content object that is appended to the current content but between the innerWraps and the rest of the wraps
2278
     *
2279
     * @param string $content Input value undergoing processing in this function.
2280
     * @param array $conf stdWrap properties for postCObject.
2281
     * @return string The processed input value
2282
     */
2283
    public function stdWrap_postCObject($content = '', $conf = [])
2284
    {
2285
        return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject');
2286
    }
2287
2288
    /**
2289
     * wrapAlign
2290
     * Wraps content with a div container having the style attribute text-align set to the given value
2291
     * See wrap
2292
     *
2293
     * @param string $content Input value undergoing processing in this function.
2294
     * @param array $conf stdWrap properties for wrapAlign.
2295
     * @return string The processed input value
2296
     */
2297
    public function stdWrap_wrapAlign($content = '', $conf = [])
2298
    {
2299
        $wrapAlign = trim($conf['wrapAlign'] ?? '');
2300
        if ($wrapAlign) {
2301
            $content = $this->wrap($content, '<div style="text-align:' . htmlspecialchars($wrapAlign) . ';">|</div>');
2302
        }
2303
        return $content;
2304
    }
2305
2306
    /**
2307
     * typolink
2308
     * Wraps the content with a link tag
2309
     * URLs and other attributes are created automatically by the values given in the stdWrap properties
2310
     * See wrap
2311
     *
2312
     * @param string $content Input value undergoing processing in this function.
2313
     * @param array $conf stdWrap properties for typolink.
2314
     * @return string The processed input value
2315
     */
2316
    public function stdWrap_typolink($content = '', $conf = [])
2317
    {
2318
        return $this->typoLink($content, $conf['typolink.']);
2319
    }
2320
2321
    /**
2322
     * wrap
2323
     * This is the "mother" of all wraps
2324
     * Third of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2325
     * Basically it will put additional content before and after the current content using a split character as a placeholder for the current content
2326
     * The default split character is | but it can be replaced with other characters by the property splitChar
2327
     * Any other wrap that does not have own splitChar settings will be using the default split char though
2328
     *
2329
     * @param string $content Input value undergoing processing in this function.
2330
     * @param array $conf stdWrap properties for wrap.
2331
     * @return string The processed input value
2332
     */
2333
    public function stdWrap_wrap($content = '', $conf = [])
2334
    {
2335
        return $this->wrap(
2336
            $content,
2337
            $conf['wrap'] ?? null,
2338
            $conf['wrap.']['splitChar'] ?? '|'
2339
        );
2340
    }
2341
2342
    /**
2343
     * noTrimWrap
2344
     * Fourth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2345
     * The major difference to any other wrap is, that this one can make use of whitespace without trimming	 *
2346
     *
2347
     * @param string $content Input value undergoing processing in this function.
2348
     * @param array $conf stdWrap properties for noTrimWrap.
2349
     * @return string The processed input value
2350
     */
2351
    public function stdWrap_noTrimWrap($content = '', $conf = [])
2352
    {
2353
        $splitChar = isset($conf['noTrimWrap.']['splitChar.'])
2354
            ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.'])
2355
            : $conf['noTrimWrap.']['splitChar'] ?? '';
2356
        if ($splitChar === null || $splitChar === '') {
2357
            $splitChar = '|';
2358
        }
2359
        $content = $this->noTrimWrap(
2360
            $content,
2361
            $conf['noTrimWrap'],
2362
            $splitChar
2363
        );
2364
        return $content;
2365
    }
2366
2367
    /**
2368
     * wrap2
2369
     * Fifth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2370
     * The default split character is | but it can be replaced with other characters by the property splitChar
2371
     *
2372
     * @param string $content Input value undergoing processing in this function.
2373
     * @param array $conf stdWrap properties for wrap2.
2374
     * @return string The processed input value
2375
     */
2376
    public function stdWrap_wrap2($content = '', $conf = [])
2377
    {
2378
        return $this->wrap(
2379
            $content,
2380
            $conf['wrap2'] ?? null,
2381
            $conf['wrap2.']['splitChar'] ?? '|'
2382
        );
2383
    }
2384
2385
    /**
2386
     * dataWrap
2387
     * Sixth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2388
     * Can fetch additional content the same way data does (i.e. {field:whatever}) and apply it to the wrap before that is applied to the content
2389
     *
2390
     * @param string $content Input value undergoing processing in this function.
2391
     * @param array $conf stdWrap properties for dataWrap.
2392
     * @return string The processed input value
2393
     */
2394
    public function stdWrap_dataWrap($content = '', $conf = [])
2395
    {
2396
        return $this->dataWrap($content, $conf['dataWrap']);
2397
    }
2398
2399
    /**
2400
     * prepend
2401
     * A content object that will be prepended to the current content after most of the wraps have already been applied
2402
     *
2403
     * @param string $content Input value undergoing processing in this function.
2404
     * @param array $conf stdWrap properties for prepend.
2405
     * @return string The processed input value
2406
     */
2407
    public function stdWrap_prepend($content = '', $conf = [])
2408
    {
2409
        return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content;
2410
    }
2411
2412
    /**
2413
     * append
2414
     * A content object that will be appended to the current content after most of the wraps have already been applied
2415
     *
2416
     * @param string $content Input value undergoing processing in this function.
2417
     * @param array $conf stdWrap properties for append.
2418
     * @return string The processed input value
2419
     */
2420
    public function stdWrap_append($content = '', $conf = [])
2421
    {
2422
        return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append');
2423
    }
2424
2425
    /**
2426
     * wrap3
2427
     * Seventh of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2428
     * The default split character is | but it can be replaced with other characters by the property splitChar
2429
     *
2430
     * @param string $content Input value undergoing processing in this function.
2431
     * @param array $conf stdWrap properties for wrap3.
2432
     * @return string The processed input value
2433
     */
2434
    public function stdWrap_wrap3($content = '', $conf = [])
2435
    {
2436
        return $this->wrap(
2437
            $content,
2438
            $conf['wrap3'] ?? null,
2439
            $conf['wrap3.']['splitChar'] ?? '|'
2440
        );
2441
    }
2442
2443
    /**
2444
     * orderedStdWrap
2445
     * Calls stdWrap for each entry in the provided array
2446
     *
2447
     * @param string $content Input value undergoing processing in this function.
2448
     * @param array $conf stdWrap properties for orderedStdWrap.
2449
     * @return string The processed input value
2450
     */
2451
    public function stdWrap_orderedStdWrap($content = '', $conf = [])
2452
    {
2453
        $sortedKeysArray = ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true);
2454
        foreach ($sortedKeysArray as $key) {
2455
            $content = $this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null);
2456
        }
2457
        return $content;
2458
    }
2459
2460
    /**
2461
     * outerWrap
2462
     * Eighth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2463
     *
2464
     * @param string $content Input value undergoing processing in this function.
2465
     * @param array $conf stdWrap properties for outerWrap.
2466
     * @return string The processed input value
2467
     */
2468
    public function stdWrap_outerWrap($content = '', $conf = [])
2469
    {
2470
        return $this->wrap($content, $conf['outerWrap'] ?? null);
2471
    }
2472
2473
    /**
2474
     * insertData
2475
     * Can fetch additional content the same way data does and replaces any occurrence of {field:whatever} with this content
2476
     *
2477
     * @param string $content Input value undergoing processing in this function.
2478
     * @return string The processed input value
2479
     */
2480
    public function stdWrap_insertData($content = '')
2481
    {
2482
        return $this->insertData($content);
2483
    }
2484
2485
    /**
2486
     * postUserFunc
2487
     * Will execute a user function after the content has been modified by any other stdWrap function
2488
     *
2489
     * @param string $content Input value undergoing processing in this function.
2490
     * @param array $conf stdWrap properties for postUserFunc.
2491
     * @return string The processed input value
2492
     */
2493
    public function stdWrap_postUserFunc($content = '', $conf = [])
2494
    {
2495
        return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'] ?? [], $content);
2496
    }
2497
2498
    /**
2499
     * postUserFuncInt
2500
     * Will execute a user function after the content has been created and each time it is fetched from Cache
2501
     * The result of this function itself will not be cached
2502
     *
2503
     * @param string $content Input value undergoing processing in this function.
2504
     * @param array $conf stdWrap properties for postUserFuncInt.
2505
     * @return string The processed input value
2506
     */
2507
    public function stdWrap_postUserFuncInt($content = '', $conf = [])
2508
    {
2509
        $substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash();
2510
        $this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [
2511
            'content' => $content,
2512
            'postUserFunc' => $conf['postUserFuncInt'],
2513
            'conf' => $conf['postUserFuncInt.'],
2514
            'type' => 'POSTUSERFUNC',
2515
            'cObj' => serialize($this),
2516
        ];
2517
        $content = '<!--' . $substKey . '-->';
2518
        return $content;
2519
    }
2520
2521
    /**
2522
     * prefixComment
2523
     * Will add HTML comments to the content to make it easier to identify certain content elements within the HTML output later on
2524
     *
2525
     * @param string $content Input value undergoing processing in this function.
2526
     * @param array $conf stdWrap properties for prefixComment.
2527
     * @return string The processed input value
2528
     */
2529
    public function stdWrap_prefixComment($content = '', $conf = [])
2530
    {
2531
        if (
2532
            (!isset($this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) || !$this->getTypoScriptFrontendController()->config['config']['disablePrefixComment'])
2533
            && !empty($conf['prefixComment'])
2534
        ) {
2535
            $content = $this->prefixComment($conf['prefixComment'], [], $content);
2536
        }
2537
        return $content;
2538
    }
2539
2540
    /**
2541
     * editIcons
2542
     * Will render icons for frontend editing as long as there is a BE user logged in
2543
     *
2544
     * @param string $content Input value undergoing processing in this function.
2545
     * @param array $conf stdWrap properties for editIcons.
2546
     * @return string The processed input value
2547
     * @deprecated since v11, will be removed with v12. Drop together with other editIcon removals.
2548
     */
2549
    public function stdWrap_editIcons($content = '', $conf = [])
2550
    {
2551
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn() && $conf['editIcons']) {
2552
            if (!isset($conf['editIcons.']) || !is_array($conf['editIcons.'])) {
2553
                $conf['editIcons.'] = [];
2554
            }
2555
            $content = $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Frontend\Conte...ctRenderer::editIcons() has been deprecated: since v11, will be removed with v12. Drop together with other editIcons removals. ( Ignorable by Annotation )

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

2555
            $content = /** @scrutinizer ignore-deprecated */ $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2556
        }
2557
        return $content;
2558
    }
2559
2560
    /**
2561
     * editPanel
2562
     * Will render the edit panel for frontend editing as long as there is a BE user logged in
2563
     *
2564
     * @param string $content Input value undergoing processing in this function.
2565
     * @param array $conf stdWrap properties for editPanel.
2566
     * @return string The processed input value
2567
     * @deprecated since v11, will be removed with v12. Drop together with other editPanel removals.
2568
     */
2569
    public function stdWrap_editPanel($content = '', $conf = [])
2570
    {
2571
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
2572
            $content = $this->editPanel($content, $conf['editPanel.']);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Frontend\Conte...ctRenderer::editPanel() has been deprecated: since v11, will be removed with v12. Drop together with other editPanel removals. ( Ignorable by Annotation )

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

2572
            $content = /** @scrutinizer ignore-deprecated */ $this->editPanel($content, $conf['editPanel.']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2573
        }
2574
        return $content;
2575
    }
2576
2577
    public function stdWrap_htmlSanitize(string $content = '', array $conf = []): string
2578
    {
2579
        $build = $conf['build'] ?? 'default';
2580
        if (class_exists($build) && is_a($build, BuilderInterface::class, true)) {
2581
            $builder = GeneralUtility::makeInstance($build);
2582
        } else {
2583
            $factory = GeneralUtility::makeInstance(SanitizerBuilderFactory::class);
2584
            $builder = $factory->build($build);
2585
        }
2586
        $sanitizer = $builder->build();
2587
        $initiator = $this->shallDebug()
2588
            ? GeneralUtility::makeInstance(SanitizerInitiator::class, DebugUtility::debugTrail())
2589
            : null;
2590
        return $sanitizer->sanitize($content, $initiator);
2591
    }
2592
2593
    /**
2594
     * Store content into cache
2595
     *
2596
     * @param string $content Input value undergoing processing in these functions.
2597
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2598
     * @return string The processed input value
2599
     */
2600
    public function stdWrap_cacheStore($content = '', $conf = [])
2601
    {
2602
        if (!isset($conf['cache.'])) {
2603
            return $content;
2604
        }
2605
        $key = $this->calculateCacheKey($conf['cache.']);
2606
        if (empty($key)) {
2607
            return $content;
2608
        }
2609
        /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
2610
        $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
2611
        $tags = $this->calculateCacheTags($conf['cache.']);
2612
        $lifetime = $this->calculateCacheLifetime($conf['cache.']);
2613
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) {
2614
            $params = [
2615
                'key' => $key,
2616
                'content' => $content,
2617
                'lifetime' => $lifetime,
2618
                'tags' => $tags,
2619
            ];
2620
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2621
            GeneralUtility::callUserFunction($_funcRef, $params, $ref);
2622
        }
2623
        $cacheFrontend->set($key, $content, $tags, $lifetime);
2624
        return $content;
2625
    }
2626
2627
    /**
2628
     * stdWrap post process hook
2629
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2630
     * this hook executes functions at after the content has been modified by the rest of the stdWrap functions but still before debugging
2631
     *
2632
     * @param string $content Input value undergoing processing in these functions.
2633
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2634
     * @return string The processed input value
2635
     */
2636
    public function stdWrap_stdWrapPostProcess($content = '', $conf = [])
2637
    {
2638
        foreach ($this->stdWrapHookObjects as $hookObject) {
2639
            /** @var ContentObjectStdWrapHookInterface $hookObject */
2640
            $content = $hookObject->stdWrapPostProcess($content, $conf, $this);
2641
        }
2642
        return $content;
2643
    }
2644
2645
    /**
2646
     * debug
2647
     * Will output the content as readable HTML code
2648
     *
2649
     * @param string $content Input value undergoing processing in this function.
2650
     * @return string The processed input value
2651
     */
2652
    public function stdWrap_debug($content = '')
2653
    {
2654
        return '<pre>' . htmlspecialchars($content) . '</pre>';
2655
    }
2656
2657
    /**
2658
     * debugFunc
2659
     * Will output the content in a debug table
2660
     *
2661
     * @param string $content Input value undergoing processing in this function.
2662
     * @param array $conf stdWrap properties for debugFunc.
2663
     * @return string The processed input value
2664
     */
2665
    public function stdWrap_debugFunc($content = '', $conf = [])
2666
    {
2667
        debug((int)$conf['debugFunc'] === 2 ? [$content] : $content);
2668
        return $content;
2669
    }
2670
2671
    /**
2672
     * debugData
2673
     * Will output the data used by the current record in a debug table
2674
     *
2675
     * @param string $content Input value undergoing processing in this function.
2676
     * @return string The processed input value
2677
     */
2678
    public function stdWrap_debugData($content = '')
2679
    {
2680
        debug($this->data, '$cObj->data:');
2681
        if (is_array($this->alternativeData)) {
0 ignored issues
show
Deprecated Code introduced by
The property TYPO3\CMS\Frontend\Conte...derer::$alternativeData has been deprecated: since v11, will be removed in v12. Drop together with usages in this class. ( Ignorable by Annotation )

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

2681
        if (is_array(/** @scrutinizer ignore-deprecated */ $this->alternativeData)) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
introduced by
The condition is_array($this->alternativeData) is always false.
Loading history...
2682
            // @deprecated since v11, will be removed in v12. Drop together with property $this->alternativeData
2683
            debug($this->alternativeData, '$this->alternativeData');
2684
        }
2685
        return $content;
2686
    }
2687
2688
    /**
2689
     * Returns number of rows selected by the query made by the properties set.
2690
     * Implements the stdWrap "numRows" property
2691
     *
2692
     * @param array $conf TypoScript properties for the property (see link to "numRows")
2693
     * @return int The number of rows found by the select
2694
     * @internal
2695
     * @see stdWrap()
2696
     */
2697
    public function numRows($conf)
2698
    {
2699
        $conf['select.']['selectFields'] = 'count(*)';
2700
        $statement = $this->exec_getQuery($conf['table'], $conf['select.']);
2701
2702
        return (int)$statement->fetchOne();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Statement::fetchOne() has been deprecated: Use Result::fetchOne() instead ( Ignorable by Annotation )

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

2702
        return (int)/** @scrutinizer ignore-deprecated */ $statement->fetchOne();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2703
    }
2704
2705
    /**
2706
     * Exploding a string by the $char value (if integer its an ASCII value) and returning index $listNum
2707
     *
2708
     * @param string $content String to explode
2709
     * @param string $listNum Index-number. You can place the word "last" in it and it will be substituted with the pointer to the last value. You can use math operators like "+-/*" (passed to calc())
2710
     * @param string $char Either a string used to explode the content string or an integer value which will then be changed into a character, eg. "10" for a linebreak char.
2711
     * @return string
2712
     */
2713
    public function listNum($content, $listNum, $char)
2714
    {
2715
        $char = $char ?: ',';
2716
        if (MathUtility::canBeInterpretedAsInteger($char)) {
2717
            $char = chr((int)$char);
2718
        }
2719
        $temp = explode($char, $content);
2720
        if (empty($temp)) {
2721
            return '';
2722
        }
2723
        $last = '' . (count($temp) - 1);
2724
        // Take a random item if requested
2725
        if ($listNum === 'rand') {
2726
            $listNum = (string)random_int(0, count($temp) - 1);
2727
        }
2728
        $index = $this->calc(str_ireplace('last', $last, $listNum));
0 ignored issues
show
Bug introduced by
It seems like str_ireplace('last', $last, $listNum) can also be of type array; however, parameter $val of TYPO3\CMS\Frontend\Conte...tObjectRenderer::calc() does only seem to accept string, 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

2728
        $index = $this->calc(/** @scrutinizer ignore-type */ str_ireplace('last', $last, $listNum));
Loading history...
2729
        return $temp[$index];
2730
    }
2731
2732
    /**
2733
     * Compares values together based on the settings in the input TypoScript array and returns the comparison result.
2734
     * Implements the "if" function in TYPO3 TypoScript
2735
     *
2736
     * @param array $conf TypoScript properties defining what to compare
2737
     * @return bool
2738
     * @see stdWrap()
2739
     * @see _parseFunc()
2740
     */
2741
    public function checkIf($conf)
2742
    {
2743
        if (!is_array($conf)) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
2744
            return true;
2745
        }
2746
        if (isset($conf['directReturn'])) {
2747
            return (bool)$conf['directReturn'];
2748
        }
2749
        $flag = true;
2750
        if (isset($conf['isNull.'])) {
2751
            $isNull = $this->stdWrap('', $conf['isNull.']);
2752
            if ($isNull !== null) {
0 ignored issues
show
introduced by
The condition $isNull !== null is always true.
Loading history...
2753
                $flag = false;
2754
            }
2755
        }
2756
        if (isset($conf['isTrue']) || isset($conf['isTrue.'])) {
2757
            $isTrue = trim((string)$this->stdWrapValue('isTrue', $conf ?? []));
2758
            if (!$isTrue) {
2759
                $flag = false;
2760
            }
2761
        }
2762
        if (isset($conf['isFalse']) || isset($conf['isFalse.'])) {
2763
            $isFalse = trim((string)$this->stdWrapValue('isFalse', $conf ?? []));
2764
            if ($isFalse) {
2765
                $flag = false;
2766
            }
2767
        }
2768
        if (isset($conf['isPositive']) || isset($conf['isPositive.'])) {
2769
            $number = $this->calc((string)$this->stdWrapValue('isPositive', $conf ?? []));
2770
            if ($number < 1) {
2771
                $flag = false;
2772
            }
2773
        }
2774
        if ($flag) {
2775
            $value = trim((string)$this->stdWrapValue('value', $conf ?? []));
2776
            if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) {
2777
                $number = trim((string)$this->stdWrapValue('isGreaterThan', $conf ?? []));
2778
                if ($number <= $value) {
2779
                    $flag = false;
2780
                }
2781
            }
2782
            if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) {
2783
                $number = trim((string)$this->stdWrapValue('isLessThan', $conf ?? []));
2784
                if ($number >= $value) {
2785
                    $flag = false;
2786
                }
2787
            }
2788
            if (isset($conf['equals']) || isset($conf['equals.'])) {
2789
                $number = trim((string)$this->stdWrapValue('equals', $conf ?? []));
2790
                if ($number != $value) {
2791
                    $flag = false;
2792
                }
2793
            }
2794
            if (isset($conf['isInList']) || isset($conf['isInList.'])) {
2795
                $number = trim((string)$this->stdWrapValue('isInList', $conf ?? []));
2796
                if (!GeneralUtility::inList($value, $number)) {
2797
                    $flag = false;
2798
                }
2799
            }
2800
            if (isset($conf['bitAnd']) || isset($conf['bitAnd.'])) {
2801
                $number = (int)trim((string)$this->stdWrapValue('bitAnd', $conf ?? []));
2802
                if ((new BitSet($number))->get($value) === false) {
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type integer expected by parameter $bitIndex of TYPO3\CMS\Core\Type\BitSet::get(). ( Ignorable by Annotation )

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

2802
                if ((new BitSet($number))->get(/** @scrutinizer ignore-type */ $value) === false) {
Loading history...
2803
                    $flag = false;
2804
                }
2805
            }
2806
        }
2807
        if ($conf['negate'] ?? false) {
2808
            $flag = !$flag;
2809
        }
2810
        return $flag;
2811
    }
2812
2813
    /**
2814
     * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser"
2815
     * together with the TypoScript options which are first converted from a TS style array
2816
     * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class.
2817
     *
2818
     * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser
2819
     * @param array $conf TypoScript properties for the parser. See link.
2820
     * @return string Return value.
2821
     * @see stdWrap()
2822
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLparserConfig()
2823
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLcleaner()
2824
     */
2825
    public function HTMLparser_TSbridge($theValue, $conf)
2826
    {
2827
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
2828
        $htmlParserCfg = $htmlParser->HTMLparserConfig($conf);
2829
        return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]);
2830
    }
2831
2832
    /**
2833
     * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes.
2834
     *
2835
     * @param string $content Input string being wrapped
2836
     * @param string $wrap The wrap string, eg. "<strong></strong>" or more likely here '<a href="index.php?id={TSFE:id}"> | </a>' which will wrap the input string in a <a> tag linking to the current page.
2837
     * @return string Output string wrapped in the wrapping value.
2838
     * @see insertData()
2839
     * @see stdWrap()
2840
     */
2841
    public function dataWrap($content, $wrap)
2842
    {
2843
        return $this->wrap($content, $this->insertData($wrap));
2844
    }
2845
2846
    /**
2847
     * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they
2848
     * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces.
2849
     * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine
2850
     * DBAL and is skipped here for later processing.
2851
     *
2852
     * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with
2853
     * the current pages title field value.
2854
     *
2855
     * @param string $str Input value
2856
     * @return string Processed input value
2857
     * @see getData()
2858
     * @see stdWrap()
2859
     * @see dataWrap()
2860
     */
2861
    public function insertData($str)
2862
    {
2863
        $inside = 0;
2864
        $newVal = '';
2865
        $pointer = 0;
2866
        $totalLen = strlen($str);
2867
        do {
2868
            if (!$inside) {
2869
                $len = strcspn(substr($str, $pointer), '{');
2870
                $newVal .= substr($str, $pointer, $len);
2871
                $inside = true;
2872
                if (substr($str, $pointer + $len + 1, 1) === '#') {
2873
                    $len2 = strcspn(substr($str, $pointer + $len), '}');
2874
                    $newVal .= substr($str, $pointer + $len, $len2);
2875
                    $len += $len2;
2876
                    $inside = false;
2877
                }
2878
            } else {
2879
                $len = strcspn(substr($str, $pointer), '}') + 1;
2880
                $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data);
2881
                $inside = false;
2882
            }
2883
            $pointer += $len;
2884
        } while ($pointer < $totalLen);
2885
        return $newVal;
2886
    }
2887
2888
    /**
2889
     * Returns a HTML comment with the second part of input string (divided by "|") where first part is an integer telling how many trailing tabs to put before the comment on a new line.
2890
     * Notice; this function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript.
2891
     *
2892
     * @param string $str Input value
2893
     * @param array $conf TypoScript Configuration (not used at this point.)
2894
     * @param string $content The content to wrap the comment around.
2895
     * @return string Processed input value
2896
     * @see stdWrap()
2897
     */
2898
    public function prefixComment($str, $conf, $content)
2899
    {
2900
        if (empty($str)) {
2901
            return $content;
2902
        }
2903
        $parts = explode('|', $str);
2904
        $indent = (int)$parts[0];
2905
        $comment = htmlspecialchars($this->insertData($parts[1]));
2906
        $output = LF
2907
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF
2908
            . str_pad('', $indent + 1, "\t") . $content . LF
2909
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF
2910
            . str_pad('', $indent + 1, "\t");
2911
        return $output;
2912
    }
2913
2914
    /**
2915
     * Implements the stdWrap property "substring" which is basically a TypoScript implementation of the PHP function, substr()
2916
     *
2917
     * @param string $content The string to perform the operation on
2918
     * @param string $options The parameters to substring, given as a comma list of integers where the first and second number is passed as arg 1 and 2 to substr().
2919
     * @return string The processed input value.
2920
     * @internal
2921
     * @see stdWrap()
2922
     */
2923
    public function substring($content, $options)
2924
    {
2925
        $options = GeneralUtility::intExplode(',', $options . ',');
2926
        if ($options[1]) {
2927
            return mb_substr($content, $options[0], $options[1], 'utf-8');
2928
        }
2929
        return mb_substr($content, $options[0], null, 'utf-8');
2930
    }
2931
2932
    /**
2933
     * Implements the stdWrap property "crop" which is a modified "substr" function allowing to limit a string length to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string really was cropped.
2934
     *
2935
     * @param string $content The string to perform the operation on
2936
     * @param string $options The parameters splitted by "|": First parameter is the max number of chars of the string. Negative value means cropping from end of string. Second parameter is the pre/postfix string to apply if cropping occurs. Third parameter is a boolean value. If set then crop will be applied at nearest space.
2937
     * @return string The processed input value.
2938
     * @internal
2939
     * @see stdWrap()
2940
     */
2941
    public function crop($content, $options)
2942
    {
2943
        $options = explode('|', $options);
2944
        $chars = (int)$options[0];
2945
        $afterstring = trim($options[1] ?? '');
2946
        $crop2space = trim($options[2] ?? '');
2947
        if ($chars) {
2948
            if (mb_strlen($content, 'utf-8') > abs($chars)) {
2949
                $truncatePosition = false;
2950
                if ($chars < 0) {
2951
                    $content = mb_substr($content, $chars, null, 'utf-8');
2952
                    if ($crop2space) {
2953
                        $truncatePosition = strpos($content, ' ');
2954
                    }
2955
                    $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content;
2956
                } else {
2957
                    $content = mb_substr($content, 0, $chars, 'utf-8');
2958
                    if ($crop2space) {
2959
                        $truncatePosition = strrpos($content, ' ');
2960
                    }
2961
                    $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring;
2962
                }
2963
            }
2964
        }
2965
        return $content;
2966
    }
2967
2968
    /**
2969
     * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length
2970
     * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string
2971
     * really was cropped.
2972
     *
2973
     * Compared to stdWrap.crop it respects HTML tags and entities.
2974
     *
2975
     * @param string $content The string to perform the operation on
2976
     * @param string $options The parameters splitted by "|": First parameter is the max number of chars of the string. Negative value means cropping from end of string. Second parameter is the pre/postfix string to apply if cropping occurs. Third parameter is a boolean value. If set then crop will be applied at nearest space.
2977
     * @return string The processed input value.
2978
     * @internal
2979
     * @see stdWrap()
2980
     * @todo Set signature to (string $content, string $options): string as breaking change in v12 and remove string
2981
     *       casts of $content and $option.
2982
     */
2983
    public function cropHTML($content, $options)
2984
    {
2985
        $content = (string)$content;
2986
        $options = (string)$options;
2987
        $options = explode('|', $options);
2988
        $chars = (int)$options[0];
2989
        $absChars = abs($chars);
2990
        $replacementForEllipsis = trim($options[1] ?? '');
2991
        $crop2space = trim($options[2] ?? '') === '1';
2992
        // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
2993
        $tags = 'a|abbr|address|area|article|aside|audio|b|bdi|bdo|blockquote|body|br|button|caption|cite|code|col|colgroup|data|datalist|dd|del|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|h1|h2|h3|h4|h5|h6|header|hr|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|main|map|mark|meter|nav|object|ol|optgroup|option|output|p|param|pre|progress|q|rb|rp|rt|rtc|ruby|s|samp|section|select|small|source|span|strong|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|tr|track|u|ul|ut|var|video|wbr';
2994
        $tagsRegEx = '
2995
			(
2996
				(?:
2997
					<!--.*?-->					# a comment
2998
					|
2999
					<canvas[^>]*>.*?</canvas>   # a canvas tag
3000
					|
3001
					<script[^>]*>.*?</script>   # a script tag
3002
					|
3003
					<noscript[^>]*>.*?</noscript> # a noscript tag
3004
					|
3005
					<template[^>]*>.*?</template> # a template tag
3006
				)
3007
				|
3008
				</?(?:' . $tags . ')+			# opening tag (\'<tag\') or closing tag (\'</tag\')
3009
				(?:
3010
					(?:
3011
						(?:
3012
							\\s+\\w[\\w-]*		# EITHER spaces, followed by attribute names
3013
							(?:
3014
								\\s*=?\\s*		# equals
3015
								(?>
3016
									".*?"		# attribute values in double-quotes
3017
									|
3018
									\'.*?\'		# attribute values in single-quotes
3019
									|
3020
									[^\'">\\s]+	# plain attribute values
3021
								)
3022
							)?
3023
						)
3024
						|						# OR a single dash (for TYPO3 link tag)
3025
						(?:
3026
							\\s+-
3027
						)
3028
					)+\\s*
3029
					|							# OR only spaces
3030
					\\s*
3031
				)
3032
				/?>								# closing the tag with \'>\' or \'/>\'
3033
			)';
3034
        $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
3035
        if ($splittedContent === false) {
3036
            $this->logger->debug('Unable to split "{content}" into tags.', ['content' => $content]);
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

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

3036
            $this->logger->/** @scrutinizer ignore-call */ 
3037
                           debug('Unable to split "{content}" into tags.', ['content' => $content]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3037
            $splittedContent = [];
3038
        }
3039
        // Reverse array if we are cropping from right.
3040
        if ($chars < 0) {
3041
            $splittedContent = array_reverse($splittedContent);
3042
        }
3043
        // Crop the text (chars of tag-blocks are not counted).
3044
        $strLen = 0;
3045
        // This is the offset of the content item which was cropped.
3046
        $croppedOffset = null;
3047
        $countSplittedContent = count($splittedContent);
3048
        // @todo $maxCroppingLength of 962 was determined by hand as the highest
3049
        //       value to not lead to internal error (Compilation failed: regular
3050
        //       expression is too large ). Still questionable if we really can
3051
        //       rely on a fixed value here, or better to say need to be understood
3052
        //       why the value has to be this value to avoid regular expression
3053
        //       compilation error.
3054
        $maxCroppingLength = 962;
3055
        for ($offset = 0; $offset < $countSplittedContent; $offset++) {
3056
            if ($offset % 2 === 0) {
3057
                $fullTempContent = $splittedContent[$offset];
3058
                $thisStrLen = mb_strlen(html_entity_decode($fullTempContent, ENT_COMPAT, 'UTF-8'), 'utf-8');
3059
                if ($strLen + $thisStrLen > $absChars) {
3060
                    $tempProcessedContent = '';
3061
                    $croppedOffset = $offset;
3062
                    $cropPosition = $absChars - $strLen;
3063
                    $cropEnd = ($cropPosition > $maxCroppingLength) ? $maxCroppingLength : $cropPosition;
3064
                    $processed = 0;
3065
                    // we need to crop in multiple steps to avoid regexp length compilation errors
3066
                    do {
3067
                        // remove already processed string part
3068
                        $tempContent = mb_substr($fullTempContent, mb_strlen($tempProcessedContent));
3069
                        $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)';
3070
                        $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropEnd + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropEnd + 1) . '}#uis';
3071
                        if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3072
                            $tempContentPlusOneCharacter = $croppedMatch[0];
3073
                        } else {
3074
                            $tempContentPlusOneCharacter = false;
3075
                        }
3076
                        $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}#uis';
3077
                        if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3078
                            $tempContent = $croppedMatch[0];
3079
                            if ($crop2space && $tempContentPlusOneCharacter !== false) {
3080
                                $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}(?=\\s)#uis';
3081
                                if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) {
3082
                                    $tempContent = $croppedMatch[0];
3083
                                }
3084
                            }
3085
                        }
3086
                        $tempProcessedContent .= $tempContent;
3087
                        $processed += $cropEnd;
3088
                        $cropEnd = ($processed + $maxCroppingLength > $cropPosition ? ($cropPosition - $processed) : $maxCroppingLength);
3089
                    } while ($cropEnd > 0 && $cropEnd < $cropPosition);
3090
                    $splittedContent[$offset] = $tempProcessedContent;
3091
                    break;
3092
                }
3093
                $strLen += $thisStrLen;
3094
            }
3095
        }
3096
        // Close cropped tags.
3097
        $closingTags = [];
3098
        if ($croppedOffset !== null) {
3099
            $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
3100
            $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
3101
            for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
3102
                if (substr($splittedContent[$offset], -2) === '/>') {
3103
                    // Ignore empty element tags (e.g. <br />).
3104
                    continue;
3105
                }
3106
                preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches);
3107
                $tagName = $matches[1] ?? null;
3108
                if ($tagName !== null) {
3109
                    // Seek for the closing (or opening) tag.
3110
                    $countSplittedContent = count($splittedContent);
3111
                    for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) {
3112
                        preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches);
3113
                        $seekingTagName = $matches[1] ?? null;
3114
                        if ($tagName === $seekingTagName) {
3115
                            // We found a matching tag.
3116
                            // Add closing tag only if it occurs after the cropped content item.
3117
                            if ($seekingOffset > $croppedOffset) {
3118
                                $closingTags[] = $splittedContent[$seekingOffset];
3119
                            }
3120
                            break;
3121
                        }
3122
                    }
3123
                }
3124
            }
3125
            // Drop the cropped items of the content array. The $closingTags will be added later on again.
3126
            array_splice($splittedContent, $croppedOffset + 1);
3127
        }
3128
        $splittedContent = array_merge($splittedContent, [
3129
            $croppedOffset !== null ? $replacementForEllipsis : '',
3130
        ], $closingTags);
3131
        // Reverse array once again if we are cropping from the end.
3132
        if ($chars < 0) {
3133
            $splittedContent = array_reverse($splittedContent);
3134
        }
3135
        return implode('', $splittedContent);
3136
    }
3137
3138
    /**
3139
     * Performs basic mathematical evaluation of the input string. Does NOT take parenthesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction())
3140
     *
3141
     * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
3142
     * @return int The result (might be a float if you did a division of the numbers).
3143
     * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()
3144
     */
3145
    public function calc($val)
3146
    {
3147
        $parts = GeneralUtility::splitCalc($val, '+-*/');
3148
        $value = 0;
3149
        foreach ($parts as $part) {
3150
            $theVal = $part[1];
3151
            $sign = $part[0];
3152
            if ((string)(int)$theVal === (string)$theVal) {
3153
                $theVal = (int)$theVal;
3154
            } else {
3155
                $theVal = 0;
3156
            }
3157
            if ($sign === '-') {
3158
                $value -= $theVal;
3159
            }
3160
            if ($sign === '+') {
3161
                $value += $theVal;
3162
            }
3163
            if ($sign === '/') {
3164
                if ((int)$theVal) {
3165
                    $value /= (int)$theVal;
3166
                }
3167
            }
3168
            if ($sign === '*') {
3169
                $value *= $theVal;
3170
            }
3171
        }
3172
        return $value;
3173
    }
3174
3175
    /**
3176
     * Implements the "split" property of stdWrap; Splits a string based on a token (given in TypoScript properties), sets the "current" value to each part and then renders a content object pointer to by a number.
3177
     * In classic TypoScript (like 'content (default)'/'styles.content (default)') this is used to render tables, splitting rows and cells by tokens and putting them together again wrapped in <td> tags etc.
3178
     * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse.
3179
     *
3180
     * @param string $value The string value to explode by $conf[token] and process each part
3181
     * @param array $conf TypoScript properties for "split
3182
     * @return string Compiled result
3183
     * @internal
3184
     * @see stdWrap()
3185
     * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::processItemStates()
3186
     */
3187
    public function splitObj($value, $conf)
3188
    {
3189
        $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'], $conf['token.']) : $conf['token'];
3190
        if ($conf['token'] === '') {
3191
            return $value;
3192
        }
3193
        $valArr = explode($conf['token'], $value);
3194
3195
        // return value directly by returnKey. No further processing
3196
        if (!empty($valArr) && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))) {
3197
            $key = (int)$this->stdWrapValue('returnKey', $conf ?? []);
3198
            return $valArr[$key] ?? '';
3199
        }
3200
3201
        // return the amount of elements. No further processing
3202
        if (!empty($valArr) && (($conf['returnCount'] ?? false) || ($conf['returnCount.'] ?? false))) {
3203
            $returnCount = (bool)$this->stdWrapValue('returnCount', $conf ?? []);
3204
            return $returnCount ? count($valArr) : 0;
3205
        }
3206
3207
        // calculate splitCount
3208
        $splitCount = count($valArr);
3209
        $max = (int)$this->stdWrapValue('max', $conf ?? []);
3210
        if ($max && $splitCount > $max) {
3211
            $splitCount = $max;
3212
        }
3213
        $min = (int)$this->stdWrapValue('min', $conf ?? []);
3214
        if ($min && $splitCount < $min) {
3215
            $splitCount = $min;
3216
        }
3217
        $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3218
        $cObjNumSplitConf = isset($conf['cObjNum.']) ? $this->stdWrap($conf['cObjNum'] ?? '', $conf['cObjNum.'] ?? []) : (string)($conf['cObjNum'] ?? '');
3219
        $splitArr = [];
3220
        if ($wrap !== '' || $cObjNumSplitConf !== '') {
3221
            $splitArr['wrap'] = $wrap;
3222
            $splitArr['cObjNum'] = $cObjNumSplitConf;
3223
            $splitArr = GeneralUtility::makeInstance(TypoScriptService::class)
3224
                ->explodeConfigurationForOptionSplit($splitArr, $splitCount);
3225
        }
3226
        $content = '';
3227
        for ($a = 0; $a < $splitCount; $a++) {
3228
            $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a;
3229
            $value = '' . $valArr[$a];
3230
            $this->data[$this->currentValKey] = $value;
3231
            if ($splitArr[$a]['cObjNum'] ?? false) {
3232
                $objName = (int)$splitArr[$a]['cObjNum'];
3233
                $value = isset($conf[$objName . '.'])
3234
                    ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.'])
3235
                    : $this->cObjGet($conf[$objName . '.'], $objName . '.');
3236
            }
3237
            $wrap = (string)$this->stdWrapValue('wrap', $splitArr[$a] ?? []);
3238
            if ($wrap) {
3239
                $value = $this->wrap($value, $wrap);
3240
            }
3241
            $content .= $value;
3242
        }
3243
        return $content;
3244
    }
3245
3246
    /**
3247
     * Processes ordered replacements on content data.
3248
     *
3249
     * @param string $content The content to be processed
3250
     * @param array $configuration The TypoScript configuration for stdWrap.replacement
3251
     * @return string The processed content data
3252
     */
3253
    protected function replacement($content, array $configuration)
3254
    {
3255
        // Sorts actions in configuration by numeric index
3256
        ksort($configuration, SORT_NUMERIC);
3257
        foreach ($configuration as $index => $action) {
3258
            // Checks whether we have a valid action and a numeric key ending with a dot ("10.")
3259
            if (is_array($action) && substr($index, -1) === '.' && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) {
3260
                $content = $this->replacementSingle($content, $action);
3261
            }
3262
        }
3263
        return $content;
3264
    }
3265
3266
    /**
3267
     * Processes a single search/replace on content data.
3268
     *
3269
     * @param string $content The content to be processed
3270
     * @param array $configuration The TypoScript of the search/replace action to be processed
3271
     * @return string The processed content data
3272
     */
3273
    protected function replacementSingle($content, array $configuration)
3274
    {
3275
        if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) {
3276
            // Gets the strings
3277
            $search = (string)$this->stdWrapValue('search', $configuration ?? []);
3278
            $replace = (string)$this->stdWrapValue('replace', $configuration, null);
3279
3280
            // Determines whether regular expression shall be used
3281
            $useRegularExpression = (bool)$this->stdWrapValue('useRegExp', $configuration, false);
3282
3283
            // Determines whether replace-pattern uses option-split
3284
            $useOptionSplitReplace = (bool)$this->stdWrapValue('useOptionSplitReplace', $configuration, false);
3285
3286
            // Performs a replacement by preg_replace()
3287
            if ($useRegularExpression) {
3288
                // Get separator-character which precedes the string and separates search-string from the modifiers
3289
                $separator = $search[0];
3290
                $startModifiers = strrpos($search, $separator);
3291
                if ($separator !== false && $startModifiers > 0) {
3292
                    $modifiers = substr($search, $startModifiers + 1);
3293
                    // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code
3294
                    $modifiers = str_replace('e', '', $modifiers);
3295
                    $search = substr($search, 0, $startModifiers + 1) . $modifiers;
3296
                }
3297
                if ($useOptionSplitReplace) {
3298
                    // init for replacement
3299
                    $splitCount = preg_match_all($search, $content, $matches);
3300
                    $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3301
                    $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3302
                    $replaceCount = 0;
3303
3304
                    $replaceCallback = static function ($match) use ($replaceArray, $search, &$replaceCount) {
3305
                        $replaceCount++;
3306
                        return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]);
3307
                    };
3308
                    $content = preg_replace_callback($search, $replaceCallback, $content);
3309
                } else {
3310
                    $content = preg_replace($search, $replace, $content);
3311
                }
3312
            } elseif ($useOptionSplitReplace) {
3313
                // turn search-string into a preg-pattern
3314
                $searchPreg = '#' . preg_quote($search, '#') . '#';
3315
3316
                // init for replacement
3317
                $splitCount = preg_match_all($searchPreg, $content, $matches);
3318
                $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3319
                $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3320
                $replaceCount = 0;
3321
3322
                $replaceCallback = static function () use ($replaceArray, &$replaceCount) {
3323
                    $replaceCount++;
3324
                    return $replaceArray[$replaceCount - 1][0];
3325
                };
3326
                $content = preg_replace_callback($searchPreg, $replaceCallback, $content);
3327
            } else {
3328
                $content = str_replace($search, $replace, $content);
3329
            }
3330
        }
3331
        return $content;
3332
    }
3333
3334
    /**
3335
     * Implements the "round" property of stdWrap
3336
     * This is a Wrapper function for PHP's rounding functions (round,ceil,floor), defaults to round()
3337
     *
3338
     * @param string $content Value to process
3339
     * @param array $conf TypoScript configuration for round
3340
     * @return string The formatted number
3341
     */
3342
    protected function round($content, array $conf = [])
3343
    {
3344
        $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3345
        $type = $this->stdWrapValue('roundType', $conf ?? []);
3346
        $floatVal = (float)$content;
3347
        switch ($type) {
3348
            case 'ceil':
3349
                $content = ceil($floatVal);
3350
                break;
3351
            case 'floor':
3352
                $content = floor($floatVal);
3353
                break;
3354
            case 'round':
3355
3356
            default:
3357
                $content = round($floatVal, $decimals);
3358
        }
3359
        return $content;
3360
    }
3361
3362
    /**
3363
     * Implements the stdWrap property "numberFormat"
3364
     * This is a Wrapper function for php's number_format()
3365
     *
3366
     * @param float $content Value to process
3367
     * @param array $conf TypoScript Configuration for numberFormat
3368
     * @return string The formatted number
3369
     */
3370
    public function numberFormat($content, $conf)
3371
    {
3372
        $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3373
        $dec_point = (string)$this->stdWrapValue('dec_point', $conf, '.');
3374
        $thousands_sep = (string)$this->stdWrapValue('thousands_sep', $conf, ',');
3375
        return number_format((float)$content, $decimals, $dec_point, $thousands_sep);
3376
    }
3377
3378
    /**
3379
     * Implements the stdWrap property, "parseFunc".
3380
     * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text
3381
     * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc.
3382
     * It is still a very important function for processing of bodytext which is normally stored in the database
3383
     * in a format which is not fully ready to be outputted.
3384
     * This situation has not become better by having a RTE around...
3385
     *
3386
     * This function is actually just splitting the input content according to the configuration of "external blocks".
3387
     * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed
3388
     * (while other parts/blocks should NOT be parsed).
3389
     * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc()
3390
     *
3391
     * @param string $theValue The value to process.
3392
     * @param array $conf TypoScript configuration for parseFunc
3393
     * @param string $ref Reference to get configuration from. Eg. "< lib.parseFunc" which means that the configuration of the object path "lib.parseFunc" will be retrieved and MERGED with what is in $conf!
3394
     * @return string The processed value
3395
     * @see _parseFunc()
3396
     */
3397
    public function parseFunc($theValue, $conf, $ref = '')
3398
    {
3399
        // Fetch / merge reference, if any
3400
        if ($ref) {
3401
            $temp_conf = [
3402
                'parseFunc' => $ref,
3403
                'parseFunc.' => $conf,
3404
            ];
3405
            $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc');
3406
            $conf = $temp_conf['parseFunc.'];
3407
        }
3408
        // early return, no processing in case no configuration is given
3409
        if (empty($conf)) {
3410
            // @deprecated Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0
3411
            trigger_error('Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0', E_USER_DEPRECATED);
3412
            return $theValue;
3413
        }
3414
        // Handle HTML sanitizer invocation
3415
        if (!isset($conf['htmlSanitize'])) {
3416
            // @deprecated Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0
3417
            trigger_error('Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0', E_USER_DEPRECATED);
3418
            $features = GeneralUtility::makeInstance(Features::class);
3419
            $conf['htmlSanitize'] = $features->isFeatureEnabled('security.frontend.htmlSanitizeParseFuncDefault');
3420
        }
3421
        $conf['htmlSanitize'] = (bool)$conf['htmlSanitize'];
3422
3423
        // Process:
3424
        if ((string)($conf['externalBlocks'] ?? '') === '') {
3425
            $result = $this->_parseFunc($theValue, $conf);
3426
            if ($conf['htmlSanitize']) {
3427
                $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
3428
            }
3429
            return $result;
3430
        }
3431
        $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks'])));
3432
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3433
        $parts = $htmlParser->splitIntoBlock($tags, $theValue);
3434
        foreach ($parts as $k => $v) {
3435
            if ($k % 2) {
3436
                // font:
3437
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3438
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3439
                if (($cfg['stripNLprev'] ?? false) || ($cfg['stripNL'] ?? false)) {
3440
                    $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]);
3441
                }
3442
                if (($cfg['stripNLnext'] ?? false) || ($cfg['stripNL'] ?? false)) {
3443
                    $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]);
3444
                }
3445
            }
3446
        }
3447
        foreach ($parts as $k => $v) {
3448
            if ($k % 2) {
3449
                $tag = $htmlParser->getFirstTag($v);
3450
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3451
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3452
                if ($cfg['callRecursive'] ?? false) {
3453
                    $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf);
3454
                    if (!($cfg['callRecursive.']['dontWrapSelf'] ?? false)) {
3455
                        if ($cfg['callRecursive.']['alternativeWrap'] ?? false) {
3456
                            $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']);
3457
                        } else {
3458
                            if (is_array($cfg['callRecursive.']['tagStdWrap.'] ?? false)) {
3459
                                $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']);
3460
                            }
3461
                            $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>';
3462
                        }
3463
                    }
3464
                } elseif ($cfg['HTMLtableCells'] ?? false) {
3465
                    $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]);
3466
                    foreach ($rowParts as $kk => $vv) {
3467
                        if ($kk % 2) {
3468
                            $colParts = $htmlParser->splitIntoBlock('td,th', $vv);
3469
                            $cc = 0;
3470
                            foreach ($colParts as $kkk => $vvv) {
3471
                                if ($kkk % 2) {
3472
                                    $cc++;
3473
                                    $tag = $htmlParser->getFirstTag($vvv);
3474
                                    $tagName = strtolower($htmlParser->getFirstTagName($vvv));
3475
                                    $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv);
3476
                                    if (($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] ?? false)
3477
                                        || (!isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && ($cfg['HTMLtableCells.']['default.']['callRecursive'] ?? false))) {
3478
                                        if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs'] ?? false) {
3479
                                            $colParts[$kkk] = str_replace(
3480
                                                '</p><p>',
3481
                                                '</p>' . LF . '<p>',
3482
                                                $colParts[$kkk]
3483
                                            );
3484
                                        }
3485
                                        $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf);
3486
                                    }
3487
                                    $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'] ?? false)
3488
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']
3489
                                        : ($cfg['HTMLtableCells.']['default.']['tagStdWrap.'] ?? null);
3490
                                    if (is_array($tagStdWrap)) {
3491
                                        $tag = $this->stdWrap($tag, $tagStdWrap);
3492
                                    }
3493
                                    $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'] ?? false)
3494
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']
3495
                                        : ($cfg['HTMLtableCells.']['default.']['stdWrap.'] ?? null);
3496
                                    if (is_array($stdWrap)) {
3497
                                        $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap);
3498
                                    }
3499
                                    $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>';
3500
                                }
3501
                            }
3502
                            $rowParts[$kk] = implode('', $colParts);
3503
                        }
3504
                    }
3505
                    $parts[$k] = implode('', $rowParts);
3506
                }
3507
                if (is_array($cfg['stdWrap.'] ?? false)) {
3508
                    $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
3509
                }
3510
            } else {
3511
                $parts[$k] = $this->_parseFunc($parts[$k], $conf);
3512
            }
3513
        }
3514
        $result = implode('', $parts);
3515
        if ($conf['htmlSanitize']) {
3516
            $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
3517
        }
3518
        return $result;
3519
    }
3520
3521
    /**
3522
     * Helper function for parseFunc()
3523
     *
3524
     * @param string $theValue The value to process.
3525
     * @param array $conf TypoScript configuration for parseFunc
3526
     * @return string The processed value
3527
     * @internal
3528
     * @see parseFunc()
3529
     */
3530
    public function _parseFunc($theValue, $conf)
3531
    {
3532
        if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
3533
            return $theValue;
3534
        }
3535
        // Indicates that the data is from within a tag.
3536
        $inside = false;
3537
        // Pointer to the total string position
3538
        $pointer = 0;
3539
        // Loaded with the current typo-tag if any.
3540
        $currentTag = null;
3541
        $stripNL = 0;
3542
        $contentAccum = [];
3543
        $contentAccumP = 0;
3544
        $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? ''));
3545
        $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? ''));
3546
        $totalLen = strlen($theValue);
3547
        do {
3548
            if (!$inside) {
3549
                if ($currentTag === null) {
3550
                    // These operations should only be performed on code outside the typotags...
3551
                    // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/'
3552
                    $len_p = 0;
3553
                    $c = 100;
3554
                    do {
3555
                        $len = strcspn(substr($theValue, $pointer + $len_p), '<');
3556
                        $len_p += $len + 1;
3557
                        $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1)));
3558
                        $c--;
3559
                    } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47);
3560
                    $len = $len_p - 1;
3561
                } else {
3562
                    $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, (string)$currentTag[0]);
3563
                }
3564
                // $data is the content until the next <tag-start or end is detected.
3565
                // In case of a currentTag set, this would mean all data between the start- and end-tags
3566
                $data = substr($theValue, $pointer, $len);
3567
                if ($data !== false) {
3568
                    if ($stripNL) {
3569
                        // If the previous tag was set to strip NewLines in the beginning of the next data-chunk.
3570
                        $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data);
3571
                        if ($data === null) {
3572
                            $this->logger->debug('Stripping new lines failed for "{data}"', ['data' => $data]);
3573
                            $data = '';
3574
                        }
3575
                    }
3576
                    // These operations should only be performed on code outside the tags...
3577
                    if (!is_array($currentTag)) {
3578
                        // Constants
3579
                        $tsfe = $this->getTypoScriptFrontendController();
3580
                        $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null;
3581
                        if (!empty($conf['constants']) && is_array($tmpConstants)) {
3582
                            foreach ($tmpConstants as $key => $val) {
3583
                                if (is_string($val)) {
3584
                                    $data = str_replace('###' . $key . '###', $val, $data);
3585
                                }
3586
                            }
3587
                        }
3588
                        // Short
3589
                        if (isset($conf['short.']) && is_array($conf['short.'])) {
3590
                            $shortWords = $conf['short.'];
3591
                            krsort($shortWords);
3592
                            foreach ($shortWords as $key => $val) {
3593
                                if (is_string($val)) {
3594
                                    $data = str_replace($key, $val, $data);
3595
                                }
3596
                            }
3597
                        }
3598
                        // stdWrap
3599
                        if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) {
3600
                            $data = $this->stdWrap($data, $conf['plainTextStdWrap.']);
3601
                        }
3602
                        // userFunc
3603
                        if ($conf['userFunc'] ?? false) {
3604
                            $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $data);
3605
                        }
3606
                        // Makelinks: (Before search-words as we need the links to be generated when searchwords go on...!)
3607
                        if ($conf['makelinks'] ?? false) {
3608
                            $data = $this->http_makelinks($data, $conf['makelinks.']['http.']);
3609
                            $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []);
3610
                        }
3611
                        // Search Words:
3612
                        // @deprecated since TYPO3 v11, will be removed in TYPO3 v12.0.
3613
                        if (($tsfe->no_cache ?? false) && ($conf['sword'] ?? false) && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) {
3614
                            if ($conf['sword'] !== '<span class="ce-sword">|</span>') {
3615
                                trigger_error('Enabling lib.parseFunc.sword will stop working in TYPO3 v12.0. Consider creating your own parser logic in a custom extension (which ideally also works with active caching.', E_USER_DEPRECATED);
3616
                            }
3617
                            $newstring = '';
3618
                            do {
3619
                                $pregSplitMode = 'i';
3620
                                // @deprecated
3621
                                // @todo: ensure these options are removed from the TypoScript reference in TYPO3 v12.0.
3622
                                if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) {
3623
                                    $pregSplitMode = '';
3624
                                }
3625
                                $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2);
3626
                                $newstring .= $pieces[0];
3627
                                $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1]));
3628
                                $inTag = false;
3629
                                if (str_contains($pieces[0], '<') || str_contains($pieces[0], '>')) {
3630
                                    // Returns TRUE, if a '<' is closer to the string-end than '>'.
3631
                                    // This is the case if we're INSIDE a tag (that could have been
3632
                                    // made by makelinks...) and we must secure, that the inside of a tag is
3633
                                    // not marked up.
3634
                                    $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>');
3635
                                }
3636
                                // The searchword:
3637
                                $match = substr($data, strlen($pieces[0]), $match_len);
3638
                                if (trim($match) && strlen($match) > 1 && !$inTag) {
3639
                                    $match = $this->wrap($match, $conf['sword'] ?? '');
3640
                                }
3641
                                // Concatenate the Search Word again.
3642
                                $newstring .= $match;
3643
                                $data = $pieces[1];
3644
                            } while ($pieces[1]);
3645
                            $data = $newstring;
3646
                        }
3647
                    }
3648
                    // Search for tags to process in current data and
3649
                    // call this method recursively if found
3650
                    if (str_contains($data, '<') && isset($conf['tags.']) && is_array($conf['tags.'])) {
3651
                        foreach ($conf['tags.'] as $tag => $tagConfig) {
3652
                            // only match tag `a` in `<a href"...">` but not in `<abbr>`
3653
                            if (preg_match('#<' . $tag . '[\s/>]#', $data)) {
3654
                                $data = $this->_parseFunc($data, $conf);
3655
                                break;
3656
                            }
3657
                        }
3658
                    }
3659
                    $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3660
                        ? $contentAccum[$contentAccumP] . $data
3661
                        : $data;
3662
                }
3663
                $inside = true;
3664
            } else {
3665
                // tags
3666
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
3667
                $data = substr($theValue, $pointer, $len);
3668
                if (str_ends_with($data, '/>') && strpos($data, '<link ') !== 0) {
3669
                    $tagContent = substr($data, 1, -2);
3670
                } else {
3671
                    $tagContent = substr($data, 1, -1);
3672
                }
3673
                $tag = explode(' ', trim($tagContent), 2);
3674
                $tag[0] = strtolower($tag[0]);
3675
                // end tag like </li>
3676
                if ($tag[0][0] === '/') {
3677
                    $tag[0] = substr($tag[0], 1);
3678
                    $tag['out'] = 1;
3679
                }
3680
                if ($conf['tags.'][$tag[0]] ?? false) {
3681
                    $treated = false;
3682
                    $stripNL = false;
3683
                    // in-tag
3684
                    if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) {
3685
                        // $currentTag (array!) is the tag we are currently processing
3686
                        $currentTag = $tag;
3687
                        $contentAccumP++;
3688
                        $treated = true;
3689
                        // in-out-tag: img and other empty tags
3690
                        if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', (string)$tag[0])) {
3691
                            $tag['out'] = 1;
3692
                        }
3693
                    }
3694
                    // out-tag
3695
                    if ($currentTag[0] === $tag[0] && isset($tag['out']) && $tag['out']) {
3696
                        $theName = $conf['tags.'][$tag[0]];
3697
                        $theConf = $conf['tags.'][$tag[0] . '.'];
3698
                        // This flag indicates, that NL- (13-10-chars) should be stripped first and last.
3699
                        $stripNL = (bool)($theConf['stripNL'] ?? false);
3700
                        // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content.
3701
                        $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false);
3702
                        $this->parameters = [];
3703
                        if (isset($currentTag[1])) {
3704
                            // decode HTML entities in attributes, since they're processed
3705
                            $params = GeneralUtility::get_tag_attributes((string)$currentTag[1], true);
3706
                            if (is_array($params)) {
3707
                                foreach ($params as $option => $val) {
3708
                                    // contains non-encoded values
3709
                                    $this->parameters[strtolower($option)] = $val;
3710
                                }
3711
                            }
3712
                            $this->parameters['allParams'] = trim((string)$currentTag[1]);
3713
                        }
3714
                        // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer.
3715
                        // $stripNL depends on the configuration of the current tag
3716
                        if ($stripNL) {
3717
                            $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1]);
3718
                            $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP]);
3719
                            $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP]);
3720
                        }
3721
                        $this->data[$this->currentValKey] = $contentAccum[$contentAccumP];
3722
                        $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]);
3723
                        // fetch the content object
3724
                        $contentAccum[$contentAccumP] = $newInput;
3725
                        $contentAccumP++;
3726
                        // If the TypoTag section
3727
                        if (!$breakOut) {
3728
                            if (!isset($contentAccum[$contentAccumP - 2])) {
3729
                                $contentAccum[$contentAccumP - 2] = '';
3730
                            }
3731
                            $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? '');
3732
                            unset($contentAccum[$contentAccumP]);
3733
                            unset($contentAccum[$contentAccumP - 1]);
3734
                            $contentAccumP -= 2;
3735
                        }
3736
                        $currentTag = null;
3737
                        $treated = true;
3738
                    }
3739
                    // other tags
3740
                    if (!$treated) {
3741
                        $contentAccum[$contentAccumP] .= $data;
3742
                    }
3743
                } else {
3744
                    // If a tag was not a typo tag, then it is just added to the content
3745
                    $stripNL = false;
3746
                    if (GeneralUtility::inList($allowTags, (string)$tag[0]) ||
3747
                        ($denyTags !== '*' && !GeneralUtility::inList($denyTags, (string)$tag[0]))) {
3748
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3749
                            ? $contentAccum[$contentAccumP] . $data
3750
                            : $data;
3751
                    } else {
3752
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3753
                            ? $contentAccum[$contentAccumP] . htmlspecialchars($data)
3754
                            : htmlspecialchars($data);
3755
                    }
3756
                }
3757
                $inside = false;
3758
            }
3759
            $pointer += $len;
3760
        } while ($pointer < $totalLen);
3761
        // Parsing nonTypoTag content (all even keys):
3762
        reset($contentAccum);
3763
        $contentAccumCount = count($contentAccum);
3764
        for ($a = 0; $a < $contentAccumCount; $a++) {
3765
            if ($a % 2 != 1) {
3766
                // stdWrap
3767
                if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) {
3768
                    $contentAccum[$a] = $this->stdWrap((string)($contentAccum[$a] ?? ''), $conf['nonTypoTagStdWrap.']);
3769
                }
3770
                // userFunc
3771
                if (!empty($conf['nonTypoTagUserFunc'])) {
3772
                    $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'] ?? [], (string)($contentAccum[$a] ?? ''));
3773
                }
3774
            }
3775
        }
3776
        return implode('', $contentAccum);
3777
    }
3778
3779
    /**
3780
     * Lets you split the content by LF and process each line independently. Used to format content made with the RTE.
3781
     *
3782
     * @param string $theValue The input value
3783
     * @param array $conf TypoScript options
3784
     * @return string The processed input value being returned; Splitted lines imploded by LF again.
3785
     * @internal
3786
     */
3787
    public function encaps_lineSplit($theValue, $conf)
3788
    {
3789
        if ((string)$theValue === '') {
3790
            return '';
3791
        }
3792
        $lParts = explode(LF, $theValue);
3793
3794
        // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line.
3795
        $lastPartIndex = count($lParts) - 1;
3796
        if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') {
3797
            array_pop($lParts);
3798
        }
3799
3800
        $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList'] ?? ''), true);
3801
        $nonWrappedTag = $conf['nonWrappedTag'];
3802
        $defaultAlign = trim((string)$this->stdWrapValue('defaultAlign', $conf ?? []));
3803
3804
        $str_content = '';
3805
        foreach ($lParts as $k => $l) {
3806
            $sameBeginEnd = 0;
3807
            $emptyTag = false;
3808
            $l = trim($l);
3809
            $attrib = [];
3810
            $nonWrapped = false;
3811
            $tagName = '';
3812
            if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') {
3813
                $fwParts = explode('>', substr($l, 1), 2);
3814
                [$tagName] = explode(' ', $fwParts[0], 2);
3815
                if (!$fwParts[1]) {
3816
                    if (substr($tagName, -1) === '/') {
3817
                        $tagName = substr($tagName, 0, -1);
3818
                    }
3819
                    if (substr($fwParts[0], -1) === '/') {
3820
                        $sameBeginEnd = 1;
3821
                        $emptyTag = true;
3822
                        // decode HTML entities, they're encoded later again
3823
                        $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true);
3824
                    }
3825
                } else {
3826
                    $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2);
3827
                    // decode HTML entities, they're encoded later again
3828
                    $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true);
3829
                    $str_content = $backParts[0];
3830
                    $sameBeginEnd = substr(strtolower($backParts[1]), 1, strlen($tagName)) === strtolower($tagName);
3831
                }
3832
            }
3833
            if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) {
3834
                $uTagName = strtoupper($tagName);
3835
                $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName);
3836
            } else {
3837
                $uTagName = strtoupper($nonWrappedTag);
3838
                // The line will be wrapped: $uTagName should not be an empty tag
3839
                $emptyTag = false;
3840
                $str_content = $lParts[$k];
3841
                $nonWrapped = true;
3842
                $attrib = [];
3843
            }
3844
            // Wrapping all inner-content:
3845
            if (is_array($conf['innerStdWrap_all.'] ?? null)) {
3846
                $str_content = $this->stdWrap($str_content, $conf['innerStdWrap_all.']);
3847
            }
3848
            if ($uTagName) {
3849
                // Setting common attributes
3850
                if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) {
3851
                    foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) {
3852
                        if (!is_array($vv)) {
3853
                            if ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'blank') {
3854
                                if ((string)($attrib[$kk] ?? '') === '') {
3855
                                    $attrib[$kk] = $vv;
3856
                                }
3857
                            } elseif ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'exists') {
3858
                                if (!isset($attrib[$kk])) {
3859
                                    $attrib[$kk] = $vv;
3860
                                }
3861
                            } else {
3862
                                $attrib[$kk] = $vv;
3863
                            }
3864
                        }
3865
                    }
3866
                }
3867
                // Wrapping all inner-content:
3868
                if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) {
3869
                    $str_content = $this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']);
3870
                }
3871
                // Default align
3872
                if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) {
3873
                    $attrib['align'] = $defaultAlign;
3874
                }
3875
                // implode (insecure) attributes, that's why `htmlspecialchars` is used here
3876
                $params = GeneralUtility::implodeAttributes($attrib, true);
3877
                if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) {
3878
                    $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
3879
                    if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) {
3880
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />';
3881
                    } else {
3882
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>';
3883
                    }
3884
                }
3885
            }
3886
            if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) {
3887
                $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']);
3888
            }
3889
            $lParts[$k] = $str_content;
3890
        }
3891
        return implode(LF, $lParts);
3892
    }
3893
3894
    /**
3895
     * Finds URLS in text and makes it to a real link.
3896
     * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link,
3897
     * linking to the URL we should have found.
3898
     *
3899
     * @param string $data The string in which to search for "http://
3900
     * @param array $conf Configuration for makeLinks, see link
3901
     * @return string The processed input string, being returned.
3902
     * @see _parseFunc()
3903
     */
3904
    public function http_makelinks($data, $conf)
3905
    {
3906
        $parts = [];
3907
        $aTagParams = $this->getATagParams($conf);
3908
        foreach (['http://', 'https://'] as $scheme) {
3909
            $textpieces = explode($scheme, $data);
3910
            $pieces = count($textpieces);
3911
            $textstr = $textpieces[0];
3912
            for ($i = 1; $i < $pieces; $i++) {
3913
                $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3914
                if (trim(substr($textstr, -1)) === '' && $len) {
3915
                    $lastChar = substr($textpieces[$i], $len - 1, 1);
3916
                    if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) {
3917
                        $len--;
3918
                    }
3919
                    // Included '\/' 3/12
3920
                    $parts[0] = substr($textpieces[$i], 0, $len);
3921
                    $parts[1] = substr($textpieces[$i], $len);
3922
                    $keep = $conf['keep'];
3923
                    $linkParts = parse_url($scheme . $parts[0]);
3924
                    $linktxt = '';
3925
                    if (str_contains($keep, 'scheme')) {
3926
                        $linktxt = $scheme;
3927
                    }
3928
                    $linktxt .= $linkParts['host'];
3929
                    if (str_contains($keep, 'path')) {
3930
                        $linktxt .= $linkParts['path'];
3931
                        // Added $linkParts['query'] 3/12
3932
                        if (str_contains($keep, 'query') && $linkParts['query']) {
3933
                            $linktxt .= '?' . $linkParts['query'];
3934
                        } elseif ($linkParts['path'] === '/') {
3935
                            $linktxt = substr($linktxt, 0, -1);
3936
                        }
3937
                    }
3938
                    $target = (string)$this->stdWrapValue('extTarget', $conf, $this->getTypoScriptFrontendController()->extTarget);
3939
3940
                    // check for jump URLs or similar
3941
                    $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf) ?? '';
3942
3943
                    $res = '<a href="' . htmlspecialchars($linkUrl) . '"'
3944
                        . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
3945
                        . $aTagParams . '>';
3946
3947
                    $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3948
                    if ((string)$conf['ATagBeforeWrap'] !== '') {
3949
                        $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
3950
                    } else {
3951
                        $res = $this->wrap($res . $linktxt . '</a>', $wrap);
3952
                    }
3953
                    $textstr .= $res . $parts[1];
3954
                } else {
3955
                    $textstr .= $scheme . $textpieces[$i];
3956
                }
3957
            }
3958
            $data = $textstr;
3959
        }
3960
        return $textstr;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $textstr seems to be defined by a foreach iteration on line 3908. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
3961
    }
3962
3963
    /**
3964
     * Will find all strings prefixed with "mailto:" in the $data string and make them into a link,
3965
     * linking to the email address they point to.
3966
     *
3967
     * @param string $data The string in which to search for "mailto:
3968
     * @param array $conf Configuration for makeLinks, see link
3969
     * @return string The processed input string, being returned.
3970
     * @see _parseFunc()
3971
     */
3972
    public function mailto_makelinks($data, $conf)
3973
    {
3974
        $conf = (array)$conf;
3975
        $parts = [];
3976
        // http-split
3977
        $aTagParams = $this->getATagParams($conf);
3978
        $textpieces = explode('mailto:', $data);
3979
        $pieces = count($textpieces);
3980
        $textstr = $textpieces[0];
3981
        $tsfe = $this->getTypoScriptFrontendController();
3982
        for ($i = 1; $i < $pieces; $i++) {
3983
            $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3984
            if (trim(substr($textstr, -1)) === '' && $len) {
3985
                $lastChar = substr($textpieces[$i], $len - 1, 1);
3986
                if (!preg_match('/[A-Za-z0-9]/', $lastChar)) {
3987
                    $len--;
3988
                }
3989
                $parts[0] = substr($textpieces[$i], 0, $len);
3990
                $parts[1] = substr($textpieces[$i], $len);
3991
                $linktxt = (string)preg_replace('/\\?.*/', '', $parts[0]);
3992
                [$mailToUrl, $linktxt, $attributes] = $this->getMailTo($parts[0], $linktxt);
3993
                $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl);
3994
                $mailtoAttrs = GeneralUtility::implodeAttributes($attributes ?? [], true);
3995
                $aTagParams .= ($mailtoAttrs !== '' ? ' ' . $mailtoAttrs : '');
3996
                $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>';
3997
                $wrap = (string)$this->stdWrapValue('wrap', $conf);
3998
                if ((string)$conf['ATagBeforeWrap'] !== '') {
3999
                    $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
4000
                } else {
4001
                    $res = $this->wrap($res . $linktxt . '</a>', $wrap);
4002
                }
4003
                $textstr .= $res . $parts[1];
4004
            } else {
4005
                $textstr .= 'mailto:' . $textpieces[$i];
4006
            }
4007
        }
4008
        return $textstr;
4009
    }
4010
4011
    /**
4012
     * Creates and returns a TypoScript "imgResource".
4013
     * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER".
4014
     * In the first case a current image is returned, possibly scaled down or otherwise processed.
4015
     * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines.
4016
     * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']);
4017
     *
4018
     * Structure of the returned info array:
4019
     *  0 => width
4020
     *  1 => height
4021
     *  2 => file extension
4022
     *  3 => file name
4023
     *  origFile => original file name
4024
     *  origFile_mtime => original file mtime
4025
     *  -- only available if processed via FAL: --
4026
     *  originalFile => original file object
4027
     *  processedFile => processed file object
4028
     *  fileCacheHash => checksum of processed file
4029
     *
4030
     * @param string|File|FileReference $file A "imgResource" TypoScript data type. Either a TypoScript file resource, a file or a file reference object or the string GIFBUILDER. See description above.
4031
     * @param array $fileArray TypoScript properties for the imgResource type
4032
     * @return array|null Returns info-array
4033
     * @see cImage()
4034
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder
4035
     */
4036
    public function getImgResource($file, $fileArray)
4037
    {
4038
        $importedFile = null;
4039
        if (empty($file) && empty($fileArray)) {
4040
            return null;
4041
        }
4042
        if (!is_array($fileArray)) {
0 ignored issues
show
introduced by
The condition is_array($fileArray) is always true.
Loading history...
4043
            $fileArray = (array)$fileArray;
4044
        }
4045
        $imageResource = null;
4046
        if ($file === 'GIFBUILDER') {
4047
            $gifCreator = GeneralUtility::makeInstance(GifBuilder::class);
4048
            $theImage = '';
4049
            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) {
4050
                $gifCreator->start($fileArray, $this->data);
4051
                $theImage = $gifCreator->gifBuild();
4052
            }
4053
            $imageResource = $gifCreator->getImageDimensions($theImage);
4054
            $imageResource['origFile'] = $theImage;
4055
        } else {
4056
            if ($file instanceof File) {
4057
                $fileObject = $file;
4058
            } elseif ($file instanceof FileReference) {
4059
                $fileObject = $file->getOriginalFile();
4060
            } else {
4061
                try {
4062
                    if (isset($fileArray['import.']) && $fileArray['import.']) {
4063
                        $importedFile = trim($this->stdWrap('', $fileArray['import.']));
4064
                        if (!empty($importedFile)) {
4065
                            $file = $importedFile;
4066
                        }
4067
                    }
4068
4069
                    if (MathUtility::canBeInterpretedAsInteger($file)) {
4070
                        $treatIdAsReference = $this->stdWrapValue('treatIdAsReference', $fileArray ?? []);
4071
                        if (!empty($treatIdAsReference)) {
4072
                            $fileReference = $this->getResourceFactory()->getFileReferenceObject($file);
4073
                            $fileObject = $fileReference->getOriginalFile();
4074
                        } else {
4075
                            $fileObject = $this->getResourceFactory()->getFileObject($file);
4076
                        }
4077
                    } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier
4078
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4079
                    } else {
4080
                        if ($importedFile && !empty($fileArray['import'])) {
4081
                            $file = $fileArray['import'] . $file;
4082
                        }
4083
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4084
                    }
4085
                } catch (Exception $exception) {
4086
                    $this->logger->warning('The image "{file}" could not be found and won\'t be included in frontend output', [
4087
                        'file' => $file,
4088
                        'exception' => $exception,
4089
                    ]);
4090
                    return null;
4091
                }
4092
            }
4093
            if ($fileObject instanceof File) {
4094
                $processingConfiguration = [];
4095
                $processingConfiguration['width'] = $this->stdWrapValue('width', $fileArray ?? []);
4096
                $processingConfiguration['height'] = $this->stdWrapValue('height', $fileArray ?? []);
4097
                $processingConfiguration['fileExtension'] = $this->stdWrapValue('ext', $fileArray ?? []);
4098
                $processingConfiguration['maxWidth'] = (int)$this->stdWrapValue('maxW', $fileArray ?? []);
4099
                $processingConfiguration['maxHeight'] = (int)$this->stdWrapValue('maxH', $fileArray ?? []);
4100
                $processingConfiguration['minWidth'] = (int)$this->stdWrapValue('minW', $fileArray ?? []);
4101
                $processingConfiguration['minHeight'] = (int)$this->stdWrapValue('minH', $fileArray ?? []);
4102
                $processingConfiguration['noScale'] = $this->stdWrapValue('noScale', $fileArray ?? []);
4103
                $processingConfiguration['additionalParameters'] = $this->stdWrapValue('params', $fileArray ?? []);
4104
                $processingConfiguration['frame'] = (int)$this->stdWrapValue('frame', $fileArray ?? []);
4105
                if (isset($fileReference)) {
4106
                    $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($fileReference, $fileArray);
4107
                } else {
4108
                    $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray);
4109
                }
4110
4111
                // Possibility to cancel/force profile extraction
4112
                // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']
4113
                if (isset($fileArray['stripProfile'])) {
4114
                    $processingConfiguration['stripProfile'] = $fileArray['stripProfile'];
4115
                }
4116
                // Check if we can handle this type of file for editing
4117
                if ($fileObject->isImage()) {
4118
                    $maskArray = $fileArray['m.'] ?? false;
4119
                    // Must render mask images and include in hash-calculating
4120
                    // - otherwise we cannot be sure the filename is unique for the setup!
4121
                    if (is_array($maskArray)) {
4122
                        $mask = $this->getImgResource($maskArray['mask'], $maskArray['mask.']);
4123
                        $bgImg = $this->getImgResource($maskArray['bgImg'], $maskArray['bgImg.']);
4124
                        $bottomImg = $this->getImgResource($maskArray['bottomImg'], $maskArray['bottomImg.']);
4125
                        $bottomImg_mask = $this->getImgResource($maskArray['bottomImg_mask'], $maskArray['bottomImg_mask.']);
4126
4127
                        $processingConfiguration['maskImages']['maskImage'] = $mask['processedFile'];
4128
                        $processingConfiguration['maskImages']['backgroundImage'] = $bgImg['processedFile'];
4129
                        $processingConfiguration['maskImages']['maskBottomImage'] = $bottomImg['processedFile'];
4130
                        $processingConfiguration['maskImages']['maskBottomImageMask'] = $bottomImg_mask['processedFile'];
4131
                    }
4132
                    $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
4133
                    if ($processedFileObject->isProcessed()) {
4134
                        $imageResource = [
4135
                            0 => (int)$processedFileObject->getProperty('width'),
4136
                            1 => (int)$processedFileObject->getProperty('height'),
4137
                            2 => $processedFileObject->getExtension(),
4138
                            3 => $processedFileObject->getPublicUrl(),
4139
                            'origFile' => $fileObject->getPublicUrl(),
4140
                            'origFile_mtime' => $fileObject->getModificationTime(),
4141
                            // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
4142
                            // in order for the setup-array to create a unique filename hash.
4143
                            'originalFile' => $fileObject,
4144
                            'processedFile' => $processedFileObject,
4145
                        ];
4146
                    }
4147
                }
4148
            }
4149
        }
4150
        // Triggered when the resolved file object isn't considered as image, processing failed and likely other scenarios
4151
        // This code path dates back to pre FAL times and should be deprecated and removed eventually
4152
        if (!isset($imageResource) && is_string($file)) {
4153
            try {
4154
                $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($file);
4155
                $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB');
4156
                $info['origFile'] = $theImage;
4157
                // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash.
4158
                $info['origFile_mtime'] = @filemtime($theImage);
4159
                $imageResource = $info;
4160
            } catch (Exception $e) {
4161
                // do nothing in case the file path is invalid
4162
            }
4163
        }
4164
        // Hook 'getImgResource': Post-processing of image resources
4165
        if (isset($imageResource)) {
4166
            /** @var ContentObjectGetImageResourceHookInterface $hookObject */
4167
            foreach ($this->getGetImgResourceHookObjects() as $hookObject) {
4168
                $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this);
4169
            }
4170
        }
4171
        return $imageResource;
4172
    }
4173
4174
    /**
4175
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4176
     * or null if the crop settings or crop area is empty.
4177
     *
4178
     * The cropArea from file reference is used, if not set in TypoScript.
4179
     *
4180
     * Example TypoScript settings:
4181
     * file.crop =
4182
     * OR
4183
     * file.crop = 50,50,100,100
4184
     * OR
4185
     * file.crop.data = file:current:crop
4186
     *
4187
     * @param FileReference $fileReference
4188
     * @param array $fileArray TypoScript properties for the imgResource type
4189
     * @return Area|null
4190
     */
4191
    protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray)
4192
    {
4193
        // Use cropping area from file reference if nothing is configured in TypoScript.
4194
        if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) {
4195
            // Set crop variant from TypoScript settings. If not set, use default.
4196
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4197
            $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant);
4198
            return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference);
4199
        }
4200
4201
        return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray);
4202
    }
4203
4204
    /**
4205
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4206
     * or null if the crop settings or crop area is empty.
4207
     *
4208
     * @param FileInterface $file
4209
     * @param array $fileArray
4210
     * @return Area|null
4211
     */
4212
    protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray)
4213
    {
4214
        /** @var Area $cropArea */
4215
        $cropArea = null;
4216
        // Resolve TypoScript configured cropping.
4217
        $cropSettings = isset($fileArray['crop.'])
4218
            ? $this->stdWrap($fileArray['crop'], $fileArray['crop.'])
4219
            : ($fileArray['crop'] ?? null);
4220
4221
        if (is_string($cropSettings)) {
4222
            // Set crop variant from TypoScript settings. If not set, use default.
4223
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4224
            // Get cropArea from CropVariantCollection, if cropSettings is a valid json.
4225
            // CropVariantCollection::create does json_decode.
4226
            $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant);
4227
            $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file);
4228
4229
            // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100
4230
            if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) {
4231
                $cropSettings = explode(',', $cropSettings);
4232
                if (count($cropSettings) === 4) {
4233
                    $stringCropArea = GeneralUtility::makeInstance(
4234
                        Area::class,
4235
                        ...$cropSettings
4236
                    );
4237
                    $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea;
4238
                }
4239
            }
4240
        }
4241
4242
        return $cropArea;
4243
    }
4244
4245
    /**
4246
     * Takes a JSON string and creates CropVariantCollection and fetches the corresponding
4247
     * CropArea for that.
4248
     *
4249
     * @param string $cropSettings
4250
     * @param string $cropVariant
4251
     * @return Area
4252
     */
4253
    protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area
4254
    {
4255
        return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant);
4256
    }
4257
4258
    /***********************************************
4259
     *
4260
     * Data retrieval etc.
4261
     *
4262
     ***********************************************/
4263
    /**
4264
     * Returns the value for the field from $this->data. If "//" is found in the $field value that token will split the field values apart and the first field having a non-blank value will be returned.
4265
     *
4266
     * @param string $field The fieldname, eg. "title" or "navtitle // title" (in the latter case the value of $this->data[navtitle] is returned if not blank, otherwise $this->data[title] will be)
4267
     * @return string|null
4268
     */
4269
    public function getFieldVal($field)
4270
    {
4271
        if (!str_contains($field, '//')) {
4272
            return $this->data[trim($field)] ?? null;
4273
        }
4274
        $sections = GeneralUtility::trimExplode('//', $field, true);
4275
        foreach ($sections as $k) {
4276
            if ((string)($this->data[$k] ?? '') !== '') {
4277
                return $this->data[$k];
4278
            }
4279
        }
4280
4281
        return '';
4282
    }
4283
4284
    /**
4285
     * Implements the TypoScript data type "getText". This takes a string with parameters and based on those a value from somewhere in the system is returned.
4286
     *
4287
     * @param string $string The parameter string, eg. "field : title" or "field : navtitle // field : title" (in the latter case and example of how the value is FIRST splitted by "//" is shown)
4288
     * @param array|null $fieldArray Alternative field array; If you set this to an array this variable will be used to look up values for the "field" key. Otherwise the current page record in $GLOBALS['TSFE']->page is used.
4289
     * @return string The value fetched
4290
     * @see getFieldVal()
4291
     */
4292
    public function getData($string, $fieldArray = null)
4293
    {
4294
        $tsfe = $this->getTypoScriptFrontendController();
4295
        if (!is_array($fieldArray)) {
4296
            $fieldArray = $tsfe->page;
4297
        }
4298
        $retVal = '';
4299
        // @todo: getData should not be called with non-string as $string. example trigger:
4300
        //        SecureHtmlRenderingTest htmlViewHelperAvoidsCrossSiteScripting set #07 PHP 8
4301
        $sections = is_string($string) ? explode('//', $string) : [];
0 ignored issues
show
introduced by
The condition is_string($string) is always true.
Loading history...
4302
        foreach ($sections as $secKey => $secVal) {
4303
            if ($retVal) {
4304
                break;
4305
            }
4306
            $parts = explode(':', $secVal, 2);
4307
            $type = strtolower(trim($parts[0]));
4308
            $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout'];
4309
            $key = trim($parts[1] ?? '');
4310
            if (($key != '') || in_array($type, $typesWithOutParameters)) {
4311
                switch ($type) {
4312
                    case 'gp':
4313
                        // Merge GET and POST and get $key out of the merged array
4314
                        $getPostArray = GeneralUtility::_GET();
4315
                        ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST());
4316
                        $retVal = $this->getGlobal($key, $getPostArray);
4317
                        break;
4318
                    case 'tsfe':
4319
                        $retVal = $this->getGlobal('TSFE|' . $key);
4320
                        break;
4321
                    case 'getenv':
4322
                        $retVal = getenv($key);
4323
                        break;
4324
                    case 'getindpenv':
4325
                        $retVal = $this->getEnvironmentVariable($key);
4326
                        break;
4327
                    case 'field':
4328
                        $retVal = $this->getGlobal($key, $fieldArray);
4329
                        break;
4330
                    case 'file':
4331
                        $retVal = $this->getFileDataKey($key);
4332
                        break;
4333
                    case 'parameters':
4334
                        $retVal = $this->parameters[$key] ?? null;
4335
                        break;
4336
                    case 'register':
4337
                        $retVal = $tsfe->register[$key] ?? null;
4338
                        break;
4339
                    case 'global':
4340
                        $retVal = $this->getGlobal($key);
4341
                        break;
4342
                    case 'level':
4343
                        $retVal = count($tsfe->tmpl->rootLine) - 1;
4344
                        break;
4345
                    case 'leveltitle':
4346
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4347
                        $pointer = (int)($keyParts[0] ?? 0);
4348
                        $slide = (string)($keyParts[1] ?? '');
4349
4350
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4351
                        $retVal = $this->rootLineValue($numericKey, 'title', strtolower($slide) === 'slide');
4352
                        break;
4353
                    case 'levelmedia':
4354
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4355
                        $pointer = (int)($keyParts[0] ?? 0);
4356
                        $slide = (string)($keyParts[1] ?? '');
4357
4358
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4359
                        $retVal = $this->rootLineValue($numericKey, 'media', strtolower($slide) === 'slide');
4360
                        break;
4361
                    case 'leveluid':
4362
                        $numericKey = $this->getKey((int)$key, $tsfe->tmpl->rootLine);
4363
                        $retVal = $this->rootLineValue($numericKey, 'uid');
4364
                        break;
4365
                    case 'levelfield':
4366
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4367
                        $pointer = (int)($keyParts[0] ?? 0);
4368
                        $field = (string)($keyParts[1] ?? '');
4369
                        $slide = (string)($keyParts[2] ?? '');
4370
4371
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4372
                        $retVal = $this->rootLineValue($numericKey, $field, strtolower($slide) === 'slide');
4373
                        break;
4374
                    case 'fullrootline':
4375
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4376
                        $pointer = (int)($keyParts[0] ?? 0);
4377
                        $field = (string)($keyParts[1] ?? '');
4378
                        $slide = (string)($keyParts[2] ?? '');
4379
4380
                        $fullKey = (int)($pointer - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine));
4381
                        if ($fullKey >= 0) {
4382
                            $retVal = $this->rootLineValue($fullKey, $field, stristr($slide, 'slide') !== false, $tsfe->rootLine);
4383
                        }
4384
                        break;
4385
                    case 'date':
4386
                        if (!$key) {
4387
                            $key = 'd/m Y';
4388
                        }
4389
                        $retVal = date($key, $GLOBALS['EXEC_TIME']);
4390
                        break;
4391
                    case 'page':
4392
                        $retVal = $tsfe->page[$key] ?? '';
4393
                        break;
4394
                    case 'pagelayout':
4395
                        $retVal = GeneralUtility::makeInstance(PageLayoutResolver::class)
4396
                            ->getLayoutForPage($tsfe->page, $tsfe->rootLine);
4397
                        break;
4398
                    case 'current':
4399
                        $retVal = $this->data[$this->currentValKey] ?? null;
4400
                        break;
4401
                    case 'db':
4402
                        $selectParts = GeneralUtility::trimExplode(':', $key);
4403
                        $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]);
4404
                        if (is_array($db_rec) && $selectParts[2]) {
4405
                            $retVal = $db_rec[$selectParts[2]];
4406
                        }
4407
                        break;
4408
                    case 'lll':
4409
                        $retVal = $tsfe->sL('LLL:' . $key);
4410
                        break;
4411
                    case 'path':
4412
                        try {
4413
                            $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key);
4414
                        } catch (Exception $e) {
4415
                            // do nothing in case the file path is invalid
4416
                            $retVal = null;
4417
                        }
4418
                        break;
4419
                    case 'cobj':
4420
                        switch ($key) {
4421
                            case 'parentRecordNumber':
4422
                                $retVal = $this->parentRecordNumber;
4423
                                break;
4424
                        }
4425
                        break;
4426
                    case 'debug':
4427
                        switch ($key) {
4428
                            case 'rootLine':
4429
                                $retVal = DebugUtility::viewArray($tsfe->tmpl->rootLine);
4430
                                break;
4431
                            case 'fullRootLine':
4432
                                $retVal = DebugUtility::viewArray($tsfe->rootLine);
4433
                                break;
4434
                            case 'data':
4435
                                $retVal = DebugUtility::viewArray($this->data);
4436
                                break;
4437
                            case 'register':
4438
                                $retVal = DebugUtility::viewArray($tsfe->register);
4439
                                break;
4440
                            case 'page':
4441
                                $retVal = DebugUtility::viewArray($tsfe->page);
4442
                                break;
4443
                        }
4444
                        break;
4445
                    case 'flexform':
4446
                        $keyParts = GeneralUtility::trimExplode(':', $key, true);
4447
                        if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) {
4448
                            $flexFormContent = $this->data[$keyParts[0]];
4449
                            if (!empty($flexFormContent)) {
4450
                                $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
4451
                                $flexFormKey = str_replace('.', '|', $keyParts[1]);
4452
                                $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent);
4453
                                $retVal = $this->getGlobal($flexFormKey, $settings);
4454
                            }
4455
                        }
4456
                        break;
4457
                    case 'session':
4458
                        $keyParts = GeneralUtility::trimExplode('|', $key, true);
4459
                        $sessionKey = array_shift($keyParts);
4460
                        $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey);
4461
                        foreach ($keyParts as $keyPart) {
4462
                            if (is_object($retVal)) {
4463
                                $retVal = $retVal->{$keyPart};
4464
                            } elseif (is_array($retVal)) {
4465
                                $retVal = $retVal[$keyPart];
4466
                            } else {
4467
                                $retVal = '';
4468
                                break;
4469
                            }
4470
                        }
4471
                        if (!is_scalar($retVal)) {
4472
                            $retVal = '';
4473
                        }
4474
                        break;
4475
                    case 'context':
4476
                        $context = GeneralUtility::makeInstance(Context::class);
4477
                        [$aspectName, $propertyName] = GeneralUtility::trimExplode(':', $key, true, 2);
4478
                        $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, '');
4479
                        if (is_array($retVal)) {
4480
                            $retVal = implode(',', $retVal);
4481
                        }
4482
                        if (!is_scalar($retVal)) {
4483
                            $retVal = '';
4484
                        }
4485
                        break;
4486
                    case 'site':
4487
                        $site = $this->getTypoScriptFrontendController()->getSite();
4488
                        if ($key === 'identifier') {
4489
                            $retVal = $site->getIdentifier();
4490
                        } elseif ($key === 'base') {
4491
                            $retVal = $site->getBase();
4492
                        } else {
4493
                            try {
4494
                                $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.');
0 ignored issues
show
Bug introduced by
The method getConfiguration() does not exist on TYPO3\CMS\Core\Site\Entity\SiteInterface. It seems like you code against a sub-type of TYPO3\CMS\Core\Site\Entity\SiteInterface such as TYPO3\CMS\Core\Site\Entity\Site. ( Ignorable by Annotation )

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

4494
                                $retVal = ArrayUtility::getValueByPath($site->/** @scrutinizer ignore-call */ getConfiguration(), $key, '.');
Loading history...
4495
                            } catch (MissingArrayPathException $exception) {
4496
                                $this->logger->warning('getData() with "{key}" failed', ['key' => $key, 'exception' => $exception]);
4497
                            }
4498
                        }
4499
                        break;
4500
                    case 'sitelanguage':
4501
                        $siteLanguage = $this->getTypoScriptFrontendController()->getLanguage();
4502
                        $config = $siteLanguage->toArray();
4503
                        if (isset($config[$key])) {
4504
                            $retVal = $config[$key] ?? '';
4505
                        }
4506
                        break;
4507
                }
4508
            }
4509
4510
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) {
4511
                $hookObject = GeneralUtility::makeInstance($className);
4512
                if (!$hookObject instanceof ContentObjectGetDataHookInterface) {
4513
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480);
4514
                }
4515
                $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4516
                $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $ref);
4517
            }
4518
        }
4519
        return $retVal;
4520
    }
4521
4522
    /**
4523
     * Gets file information. This is a helper function for the getData() method above, which resolves e.g.
4524
     * page.10.data = file:current:title
4525
     * or
4526
     * page.10.data = file:17:title
4527
     *
4528
     * @param string $key A colon-separated key, e.g. 17:name or current:sha1, with the first part being a sys_file uid or the keyword "current" and the second part being the key of information to get from file (e.g. "title", "size", "description", etc.)
4529
     * @return string|int The value as retrieved from the file object.
4530
     */
4531
    protected function getFileDataKey($key)
4532
    {
4533
        [$fileUidOrCurrentKeyword, $requestedFileInformationKey] = GeneralUtility::trimExplode(':', $key, false, 3);
4534
        try {
4535
            if ($fileUidOrCurrentKeyword === 'current') {
4536
                $fileObject = $this->getCurrentFile();
4537
            } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) {
4538
                /** @var ResourceFactory $fileFactory */
4539
                $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
4540
                $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword);
4541
            } else {
4542
                $fileObject = null;
4543
            }
4544
        } catch (Exception $exception) {
4545
            $this->logger->warning('The file "{uid}" could not be found and won\'t be included in frontend output', ['uid' => $fileUidOrCurrentKeyword, 'exception' => $exception]);
4546
            $fileObject = null;
4547
        }
4548
4549
        if ($fileObject instanceof FileInterface) {
4550
            // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here:
4551
            switch ($requestedFileInformationKey) {
4552
                case 'name':
4553
                    return $fileObject->getName();
4554
                case 'uid':
4555
                    if (method_exists($fileObject, 'getUid')) {
4556
                        return $fileObject->getUid();
4557
                    }
4558
                    return 0;
4559
                case 'originalUid':
4560
                    if ($fileObject instanceof FileReference) {
0 ignored issues
show
introduced by
$fileObject is never a sub-type of TYPO3\CMS\Core\Resource\FileReference.
Loading history...
4561
                        return $fileObject->getOriginalFile()->getUid();
4562
                    }
4563
                    return null;
4564
                case 'size':
4565
                    return $fileObject->getSize();
4566
                case 'sha1':
4567
                    return $fileObject->getSha1();
4568
                case 'extension':
4569
                    return $fileObject->getExtension();
4570
                case 'mimetype':
4571
                    return $fileObject->getMimeType();
4572
                case 'contents':
4573
                    return $fileObject->getContents();
4574
                case 'publicUrl':
4575
                    return $fileObject->getPublicUrl();
4576
                default:
4577
                    // Generic alternative here
4578
                    return $fileObject->getProperty($requestedFileInformationKey);
4579
            }
4580
        } else {
4581
            // @todo fail silently as is common in tslib_content
4582
            return 'Error: no file object';
4583
        }
4584
    }
4585
4586
    /**
4587
     * Returns a value from the current rootline (site) from $GLOBALS['TSFE']->tmpl->rootLine;
4588
     *
4589
     * @param int $key Which level in the root line
4590
     * @param string $field The field in the rootline record to return (a field from the pages table)
4591
     * @param bool $slideBack If set, then we will traverse through the rootline from outer level towards the root level until the value found is TRUE
4592
     * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array
4593
     * @return string The value from the field of the rootline.
4594
     * @internal
4595
     * @see getData()
4596
     */
4597
    public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '')
4598
    {
4599
        $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine;
4600
        if (!$slideBack) {
4601
            return $rootLine[$key][$field] ?? '';
4602
        }
4603
        for ($a = $key; $a >= 0; $a--) {
4604
            $val = $rootLine[$a][$field] ?? '';
4605
            if ($val) {
4606
                return $val;
4607
            }
4608
        }
4609
4610
        return '';
4611
    }
4612
4613
    /**
4614
     * Return global variable where the input string $var defines array keys separated by "|"
4615
     * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
4616
     *
4617
     * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
4618
     * @param array $source Alternative array than $GLOBAL to get variables from.
4619
     * @return mixed Whatever value. If none, then blank string.
4620
     * @see getData()
4621
     */
4622
    public function getGlobal($keyString, $source = null)
4623
    {
4624
        $keys = explode('|', $keyString);
4625
        $numberOfLevels = count($keys);
4626
        $rootKey = trim($keys[0]);
4627
        $value = isset($source) ? ($source[$rootKey] ?? '') : ($GLOBALS[$rootKey] ?? '');
4628
        for ($i = 1; $i < $numberOfLevels && isset($value); $i++) {
4629
            $currentKey = trim($keys[$i]);
4630
            if (is_object($value)) {
4631
                $value = $value->{$currentKey};
4632
            } elseif (is_array($value)) {
4633
                $value = $value[$currentKey] ?? '';
4634
            } else {
4635
                $value = '';
4636
                break;
4637
            }
4638
        }
4639
        if (!is_scalar($value)) {
4640
            $value = '';
4641
        }
4642
        return $value;
4643
    }
4644
4645
    /**
4646
     * Processing of key values pointing to entries in $arr; Here negative values are converted to positive keys pointer to an entry in the array but from behind (based on the negative value).
4647
     * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost...
4648
     *
4649
     * @param int $key The integer to transform
4650
     * @param array $arr array in which the key should be found.
4651
     * @return int The processed integer key value.
4652
     * @internal
4653
     * @see getData()
4654
     */
4655
    public function getKey($key, $arr)
4656
    {
4657
        $key = (int)$key;
4658
        if (is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
4659
            if ($key < 0) {
4660
                $key = count($arr) + $key;
4661
            }
4662
            if ($key < 0) {
4663
                $key = 0;
4664
            }
4665
        }
4666
        return $key;
4667
    }
4668
4669
    /***********************************************
4670
     *
4671
     * Link functions (typolink)
4672
     *
4673
     ***********************************************/
4674
    /**
4675
     * called from the typoLink() function
4676
     *
4677
     * does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1"
4678
     * into separate parts
4679
     *
4680
     * @param string $linkText The string (text) to link
4681
     * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
4682
     * @param array $configuration TypoScript configuration
4683
     * @return array|string
4684
     * @see typoLink()
4685
     *
4686
     * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links
4687
     */
4688
    protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
4689
    {
4690
        // Link parameter value = first part
4691
        $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
4692
4693
        // Check for link-handler keyword
4694
        $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2);
4695
        $linkHandlerKeyword = (string)($linkHandlerExploded[0] ?? '');
4696
4697
        if (in_array(strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
4698
            // Disallow insecure scheme's like javascript: or data:
4699
            return $linkText;
4700
        }
4701
4702
        // additional parameters that need to be set
4703
        if ($linkParameterParts['additionalParams'] !== '') {
4704
            $forceParams = $linkParameterParts['additionalParams'];
4705
            // params value
4706
            $configuration['additionalParams'] = ($configuration['additionalParams'] ?? '') . $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
4707
        }
4708
4709
        return [
4710
            'href'   => $linkParameterParts['url'],
4711
            'target' => $linkParameterParts['target'],
4712
            'class'  => $linkParameterParts['class'],
4713
            'title'  => $linkParameterParts['title'],
4714
        ];
4715
    }
4716
4717
    /**
4718
     * Implements the "typolink" property of stdWrap (and others)
4719
     * Basically the input string, $linktext, is (typically) wrapped in a <a>-tag linking to some page, email address, file or URL based on a parameter defined by the configuration array $conf.
4720
     * This function is best used from internal functions as is. There are some API functions defined after this function which is more suited for general usage in external applications.
4721
     * Generally the concept "typolink" should be used in your own applications as an API for making links to pages with parameters and more. The reason for this is that you will then automatically make links compatible with all the centralized functions for URL simulation and manipulation of parameters into hashes and more.
4722
     * For many more details on the parameters and how they are interpreted, please see the link to TSref below.
4723
     *
4724
     * the FAL API is handled with the namespace/prefix "file:..."
4725
     *
4726
     * @param string $linkText The string (text) to link
4727
     * @param array $conf TypoScript configuration (see link below)
4728
     * @return string A link-wrapped string.
4729
     * @see stdWrap()
4730
     * @see \TYPO3\CMS\Frontend\Plugin\AbstractPlugin::pi_linkTP()
4731
     */
4732
    public function typoLink($linkText, $conf)
4733
    {
4734
        $linkText = (string)$linkText;
4735
        $tsfe = $this->getTypoScriptFrontendController();
4736
4737
        $linkParameter = trim((string)$this->stdWrapValue('parameter', $conf ?? []));
4738
        $this->lastTypoLinkUrl = '';
4739
        $this->lastTypoLinkTarget = '';
4740
4741
        $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf);
4742
4743
        // check if the link handler hook has resolved the link completely already
4744
        if (!is_array($resolvedLinkParameters)) {
4745
            return $resolvedLinkParameters;
4746
        }
4747
        $linkParameter = $resolvedLinkParameters['href'];
4748
        $target = $resolvedLinkParameters['target'];
4749
        $title = $resolvedLinkParameters['title'];
4750
4751
        if (!$linkParameter) {
4752
            return $this->resolveAnchorLink($linkText, $conf ?? []);
4753
        }
4754
4755
        // Detecting kind of link and resolve all necessary parameters
4756
        $linkService = GeneralUtility::makeInstance(LinkService::class);
4757
        try {
4758
            $linkDetails = $linkService->resolve($linkParameter);
4759
        } catch (UnknownLinkHandlerException | InvalidPathException $exception) {
4760
            $this->logger->warning('The link could not be generated', ['exception' => $exception]);
4761
            return $linkText;
4762
        }
4763
4764
        $linkDetails['typoLinkParameter'] = $linkParameter;
4765
        if (isset($linkDetails['type']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
4766
            /** @var AbstractTypolinkBuilder $linkBuilder */
4767
            $linkBuilder = GeneralUtility::makeInstance(
4768
                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
4769
                $this,
4770
                // AbstractTypolinkBuilder type hints an optional dependency to TypoScriptFrontendController.
4771
                // Some core parts however "fake" $GLOBALS['TSFE'] to stdCLass() due to its long list of
4772
                // dependencies. f:html view helper is such a scenario. This of course crashes if given to typolink builder
4773
                // classes. For now, we check the instance and hand over 'null', giving the link builders the option
4774
                // to take care of tsfe themselfs. This scenario is for instance triggered when in BE login when sys_news
4775
                // records set links.
4776
                $tsfe instanceof TypoScriptFrontendController ? $tsfe : null
4777
            );
4778
            try {
4779
                $linkedResult = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
4780
                // Legacy layer, can be removed in TYPO3 v12.0.
4781
                if (!($linkedResult instanceof LinkResultInterface)) {
4782
                    if (is_array($linkedResult)) {
0 ignored issues
show
introduced by
The condition is_array($linkedResult) is always true.
Loading history...
4783
                        [$url, $linkText, $target] = $linkedResult;
4784
                    } else {
4785
                        $url = '';
4786
                    }
4787
                    $linkedResult = new LinkResult($linkDetails['type'], $url);
4788
                    $linkedResult = $linkedResult
4789
                        ->withTarget($target)
4790
                        ->withLinkConfiguration($conf)
4791
                        ->withLinkText($linkText);
4792
                }
4793
            } catch (UnableToLinkException $e) {
4794
                $this->logger->debug('Unable to link "{text}"', [
4795
                    'text' => $e->getLinkText(),
4796
                    'exception' => $e,
4797
                ]);
4798
4799
                // Only return the link text directly
4800
                return $e->getLinkText();
4801
            }
4802
        } elseif (isset($linkDetails['url'])) {
4803
            $linkedResult = new LinkResult($linkDetails['type'], $linkDetails['url']);
4804
            $linkedResult = $linkedResult
4805
                ->withTarget($target)
4806
                ->withLinkConfiguration($conf)
4807
                ->withLinkText($linkText);
4808
        } else {
4809
            return $linkText;
4810
        }
4811
4812
        $this->lastTypoLinkResult = $linkedResult;
4813
        $this->lastTypoLinkTarget = $linkedResult->getTarget();
4814
        $this->lastTypoLinkUrl = $linkedResult->getUrl();
4815
        $this->lastTypoLinkLD['target'] = htmlspecialchars($linkedResult->getTarget());
4816
        $this->lastTypoLinkLD['totalUrl'] = $linkedResult->getUrl();
4817
        $this->lastTypoLinkLD['type'] = $linkedResult->getType();
4818
4819
        // We need to backup the URL because ATagParams might call typolink again and change the last URL.
4820
        $url = $this->lastTypoLinkUrl;
4821
        $linkResultAttrs = array_filter(
4822
            $linkedResult->getAttributes(),
4823
            static function (string $name): bool {
4824
                return !in_array($name, ['href', 'target']);
4825
            },
4826
            ARRAY_FILTER_USE_KEY
4827
        );
4828
        $finalTagParts = [
4829
            'aTagParams' => rtrim($this->getATagParams($conf) . ' ' . GeneralUtility::implodeAttributes($linkResultAttrs, true)),
4830
            'url'        => $url,
4831
            'TYPE'       => $linkedResult->getType(),
4832
        ];
4833
4834
        // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
4835
        if (!empty($finalTagParts['aTagParams'])) {
4836
            $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'], true);
4837
            if (isset($aTagParams['href'])) {
4838
                unset($aTagParams['href']);
4839
                $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams, true);
4840
            }
4841
        }
4842
4843
        // Building the final <a href=".."> tag
4844
        $tagAttributes = [];
4845
4846
        // Title attribute
4847
        if (empty($title)) {
4848
            $title = $conf['title'] ?? '';
4849
            if (isset($conf['title.']) && is_array($conf['title.'])) {
4850
                $title = $this->stdWrap($title, $conf['title.']);
4851
            }
4852
        }
4853
4854
        // Check, if the target is coded as a JS open window link:
4855
        $JSwindowParts = [];
4856
        $JSwindowParams = '';
4857
        if ($this->lastTypoLinkResult->getTarget() && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $this->lastTypoLinkResult->getTarget(), $JSwindowParts)) {
0 ignored issues
show
Bug introduced by
The method getTarget() does not exist on null. ( Ignorable by Annotation )

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

4857
        if ($this->lastTypoLinkResult->/** @scrutinizer ignore-call */ getTarget() && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $this->lastTypoLinkResult->getTarget(), $JSwindowParts)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4858
            // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
4859
            $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower(($conf['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
4860
            $JSwindow_paramsArr = [];
4861
            $target = $conf['target'] ?? 'FEopenLink';
4862
            foreach ($JSwindow_tempParamsArr as $JSv) {
4863
                [$JSp, $JSv] = explode('=', $JSv, 2);
4864
                // If the target is set as JS param, this is extracted
4865
                if ($JSp === 'target') {
4866
                    $target = $JSv;
4867
                } else {
4868
                    $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
4869
                }
4870
            }
4871
            $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttribute('target', $target);
4872
            // Add width/height:
4873
            $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
4874
            $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
4875
            // Imploding into string:
4876
            $JSwindowParams = implode(',', $JSwindow_paramsArr);
4877
        }
4878
4879
        if (!$JSwindowParams && $linkedResult->getType() === LinkService::TYPE_EMAIL && $tsfe instanceof TypoScriptFrontendController && $tsfe->spamProtectEmailAddresses === 'ascii') {
4880
            $tagAttributes['href'] = $finalTagParts['url'];
4881
        } else {
4882
            $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']);
4883
        }
4884
        if (!empty($title)) {
4885
            $tagAttributes['title'] = htmlspecialchars($title);
4886
            $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttribute('title', $title);
4887
        }
4888
4889
        // Target attribute
4890
        if (!empty($this->lastTypoLinkResult->getTarget())) {
4891
            $tagAttributes['target'] = htmlspecialchars($this->lastTypoLinkResult->getTarget());
4892
        }
4893
        if ($JSwindowParams && $tsfe instanceof TypoScriptFrontendController && in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
4894
            // Create TARGET-attribute only if the right doctype is used
4895
            unset($tagAttributes['target']);
4896
        }
4897
4898
        if ($JSwindowParams) {
4899
            $JSwindowAttrs = [
4900
                'data-window-url' => $tsfe instanceof TypoScriptFrontendController ? $tsfe->baseUrlWrap($finalTagParts['url']) : $finalTagParts['url'],
4901
                'data-window-target' => $this->lastTypoLinkResult->getTarget(),
4902
                'data-window-features' => $JSwindowParams,
4903
            ];
4904
            $tagAttributes = array_merge($tagAttributes, array_map('htmlspecialchars', $JSwindowAttrs));
4905
            $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttributes($JSwindowAttrs);
4906
            $this->addDefaultFrontendJavaScript();
4907
        }
4908
4909
        if (!empty($resolvedLinkParameters['class'])) {
4910
            $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']);
4911
            $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttribute('class', $tagAttributes['class']);
4912
        }
4913
4914
        // Prevent trouble with double and missing spaces between attributes and merge params before implode
4915
        // (skip decoding HTML entities, since `$tagAttributes` are expected to be encoded already)
4916
        $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
4917
        $finalTagAttributes = $this->addSecurityRelValues($finalTagAttributes, $this->lastTypoLinkResult->getTarget(), $tagAttributes['href']);
4918
        $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttributes($finalTagAttributes);
4919
        $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
4920
4921
        $this->lastTypoLinkTarget = $this->lastTypoLinkResult->getTarget();
4922
        // kept for backwards-compatibility in hooks
4923
        $finalTagParts['targetParams'] = $this->lastTypoLinkResult->getTarget() ? 'target="' . htmlspecialchars($this->lastTypoLinkResult->getTarget()) . '"' : '';
4924
4925
        // Call user function:
4926
        if ($conf['userFunc'] ?? false) {
4927
            $finalTagParts['TAG'] = $finalAnchorTag;
4928
            $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $finalTagParts);
4929
            // Ensure to keep the result object up-to-date even after the user func was called
4930
            $finalAnchorTagParts = GeneralUtility::get_tag_attributes($finalAnchorTag, true);
4931
            $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttributes($finalAnchorTagParts, true);
4932
        }
4933
4934
        // Hook: Call post processing function for link rendering:
4935
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'])) {
4936
            $_params = [
4937
                'conf' => &$conf,
4938
                'linktxt' => &$linkText,
4939
                'finalTag' => &$finalAnchorTag,
4940
                'finalTagParts' => &$finalTagParts,
4941
                'linkDetails' => &$linkDetails,
4942
                'tagAttributes' => &$finalTagAttributes,
4943
            ];
4944
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) {
4945
                $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4946
                GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
4947
            }
4948
            // Ensure to keep the result object up-to-date even after the user func was called
4949
            $finalAnchorTagParts = GeneralUtility::get_tag_attributes($finalAnchorTag, true);
4950
            $this->lastTypoLinkResult = $this->lastTypoLinkResult
4951
                ->withAttributes($finalAnchorTagParts)
4952
                ->withLinkText((string)$_params['linktxt']);
4953
        }
4954
4955
        // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made:
4956
        if ($conf['returnLast'] ?? false) {
4957
            switch ($conf['returnLast']) {
4958
                case 'url':
4959
                    return $this->lastTypoLinkUrl;
4960
                case 'target':
4961
                    return $this->lastTypoLinkTarget;
4962
                case 'result':
4963
                    return $this->lastTypoLinkResult;
4964
            }
4965
        }
4966
4967
        $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
4968
4969
        if ($conf['ATagBeforeWrap'] ?? false) {
4970
            return $finalAnchorTag . $this->wrap((string)$this->lastTypoLinkResult->getLinkText(), $wrap) . '</a>';
4971
        }
4972
        return $this->wrap($finalAnchorTag . $this->lastTypoLinkResult->getLinkText() . '</a>', $wrap);
4973
    }
4974
4975
    protected function addSecurityRelValues(array $tagAttributes, ?string $target, string $url): array
4976
    {
4977
        $relAttribute = 'noreferrer';
4978
        if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($url)) {
4979
            return $tagAttributes;
4980
        }
4981
4982
        if (!isset($tagAttributes['rel'])) {
4983
            $tagAttributes['rel'] = $relAttribute;
4984
            return $tagAttributes;
4985
        }
4986
4987
        $tagAttributes['rel'] = implode(' ', array_unique(array_merge(
4988
            GeneralUtility::trimExplode(' ', $relAttribute),
4989
            GeneralUtility::trimExplode(' ', $tagAttributes['rel'])
4990
        )));
4991
4992
        return $tagAttributes;
4993
    }
4994
4995
    /**
4996
     * Checks whether the given url is an internal url.
4997
     *
4998
     * It will check the host part only, against all configured sites
4999
     * whether the given host is any. If so, the url is considered internal
5000
     *
5001
     * @param string $url The url to check.
5002
     * @return bool
5003
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
5004
     */
5005
    protected function isInternalUrl(string $url): bool
5006
    {
5007
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
5008
        $parsedUrl = parse_url($url);
5009
        $foundDomains = 0;
5010
        if (!isset($parsedUrl['host'])) {
5011
            return true;
5012
        }
5013
5014
        $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
5015
5016
        if ($cache->has($cacheIdentifier) === false) {
5017
            foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
5018
                if ($site->getBase()->getHost() === $parsedUrl['host']) {
5019
                    ++$foundDomains;
5020
                    break;
5021
                }
5022
5023
                if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url)) {
5024
                    ++$foundDomains;
5025
                    break;
5026
                }
5027
            }
5028
5029
            $cache->set($cacheIdentifier, $foundDomains > 0);
5030
        }
5031
5032
        return (bool)$cache->get($cacheIdentifier);
5033
    }
5034
5035
    /**
5036
     * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
5037
     *
5038
     * @param array $conf TypoScript properties for "typolink
5039
     * @return string The URL of the link-tag that typolink() would by itself return
5040
     * @see typoLink()
5041
     */
5042
    public function typoLink_URL($conf)
5043
    {
5044
        $this->typoLink('|', $conf);
5045
        return $this->lastTypoLinkUrl;
5046
    }
5047
5048
    /**
5049
     * Returns a linked string made from typoLink parameters.
5050
     *
5051
     * This function takes $label as a string, wraps it in a link-tag based on the $params string, which should contain data like that you would normally pass to the popular <LINK>-tag in the TSFE.
5052
     * Optionally you can supply $urlParameters which is an array with key/value pairs that are rawurlencoded and appended to the resulting url.
5053
     *
5054
     * @param string $label Text string being wrapped by the link.
5055
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
5056
     * @param array|string $urlParameters As an array key/value pairs represent URL parameters to set. Values NOT URL-encoded yet, keys should be URL-encoded if needed. As a string the parameter is expected to be URL-encoded already.
5057
     * @param string $target Specific target set, if any. (Default is using the current)
5058
     * @return string The wrapped $label-text string
5059
     * @see getTypoLink_URL()
5060
     */
5061
    public function getTypoLink($label, $params, $urlParameters = [], $target = '')
5062
    {
5063
        $conf = [];
5064
        $conf['parameter'] = $params;
5065
        if ($target) {
5066
            $conf['target'] = $target;
5067
            $conf['extTarget'] = $target;
5068
            $conf['fileTarget'] = $target;
5069
        }
5070
        if (is_array($urlParameters)) {
5071
            if (!empty($urlParameters)) {
5072
                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
5073
            }
5074
        } else {
5075
            $conf['additionalParams'] = ($conf['additionalParams'] ?? '') . $urlParameters;
5076
        }
5077
        $out = $this->typoLink($label, $conf);
5078
        return $out;
5079
    }
5080
5081
    /**
5082
     * Returns the canonical URL to the current "location", which include the current page ID and type
5083
     * and optionally the query string
5084
     *
5085
     * @param bool $addQueryString Whether additional GET arguments in the query string should be included or not
5086
     * @return string
5087
     */
5088
    public function getUrlToCurrentLocation($addQueryString = true)
5089
    {
5090
        $conf = [];
5091
        $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type;
5092
        if ($addQueryString) {
5093
            $conf['addQueryString'] = '1';
5094
            $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars)));
5095
            $conf['addQueryString.'] = [
5096
                'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : ''),
5097
            ];
5098
        }
5099
5100
        return $this->typoLink_URL($conf);
5101
    }
5102
5103
    /**
5104
     * Returns the URL of a "typolink" create from the input parameter string, url-parameters and target
5105
     *
5106
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
5107
     * @param array|string $urlParameters As an array key/value pairs represent URL parameters to set. Values NOT URL-encoded yet, keys should be URL-encoded if needed. As a string the parameter is expected to be URL-encoded already.
5108
     * @param string $target Specific target set, if any. (Default is using the current)
5109
     * @return string The URL
5110
     * @see getTypoLink()
5111
     */
5112
    public function getTypoLink_URL($params, $urlParameters = [], $target = '')
5113
    {
5114
        $this->getTypoLink('', $params, $urlParameters, $target);
5115
        return $this->lastTypoLinkUrl;
5116
    }
5117
5118
    /**
5119
     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
5120
     *
5121
     * @param string $context The context in which the method is called (e.g. typoLink).
5122
     * @param string $url The URL that should be processed.
5123
     * @param array $typolinkConfiguration The current link configuration array.
5124
     * @return string|null Returns NULL if URL was not processed or the processed URL as a string.
5125
     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
5126
     */
5127
    protected function processUrl($context, $url, $typolinkConfiguration = [])
5128
    {
5129
        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
5130
        if (empty($urlProcessors)) {
5131
            return $url;
5132
        }
5133
5134
        foreach ($urlProcessors as $identifier => $configuration) {
5135
            if (empty($configuration) || !is_array($configuration)) {
5136
                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
5137
            }
5138
            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
5139
                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
5140
            }
5141
        }
5142
5143
        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
5144
        $keepProcessing = true;
5145
5146
        foreach ($orderedProcessors as $configuration) {
5147
            /** @var UrlProcessorInterface $urlProcessor */
5148
            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
5149
            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
5150
            if (!$keepProcessing) {
5151
                break;
5152
            }
5153
        }
5154
5155
        return $url;
5156
    }
5157
5158
    /**
5159
     * Creates a href attibute for given $mailAddress.
5160
     * The function uses spamProtectEmailAddresses for encoding the mailto statement.
5161
     * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:[email protected]".
5162
     *
5163
     * Returns an array with three items (numeric index)
5164
     *   #0: $mailToUrl (string), ready to be inserted into the href attribute of the <a> tag
5165
     *   #1: $linktxt (string), content between starting and ending `<a>` tag
5166
     *   #2: $attributes (array<string, string>), additional attributes for `<a>` tag
5167
     *
5168
     * @param string $mailAddress Email address
5169
     * @param string $linktxt Link text, default will be the email address.
5170
     * @return array{0: string, 1: string, 2: array<string, string>} A numerical array with three items
5171
     */
5172
    public function getMailTo($mailAddress, $linktxt)
5173
    {
5174
        $mailAddress = (string)$mailAddress;
5175
        if ((string)$linktxt === '') {
5176
            $linktxt = htmlspecialchars($mailAddress);
5177
        }
5178
5179
        $originalMailToUrl = 'mailto:' . $mailAddress;
5180
        $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
5181
        $attributes = [];
5182
5183
        // no processing happened, therefore, the default processing kicks in
5184
        if ($mailToUrl === $originalMailToUrl) {
5185
            $tsfe = $this->getTypoScriptFrontendController();
5186
            if ($tsfe instanceof TypoScriptFrontendController && $tsfe->spamProtectEmailAddresses) {
5187
                $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
5188
                if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
5189
                    $attributes = [
5190
                        'data-mailto-token' => $mailToUrl,
5191
                        'data-mailto-vector' => (int)$tsfe->spamProtectEmailAddresses,
5192
                    ];
5193
                    $mailToUrl = '#';
5194
                }
5195
                $atLabel = '(at)';
5196
                if (($atLabelFromConfig = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst'] ?? '')) !== '') {
5197
                    $atLabel = $atLabelFromConfig;
5198
                }
5199
                $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
5200
                if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst'] ?? false) {
5201
                    $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
5202
                    $lastDotLabel = $lastDotLabel ?: '(dot)';
5203
                    $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
5204
                    if ($spamProtectedMailAddress === null) {
5205
                        $this->logger->debug('Error replacing the last dot in email address "{email}"', ['email' => $spamProtectedMailAddress]);
5206
                        $spamProtectedMailAddress = '';
5207
                    }
5208
                }
5209
                $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
5210
                $this->addDefaultFrontendJavaScript();
5211
            }
5212
        }
5213
5214
        return [$mailToUrl, $linktxt, $attributes];
5215
    }
5216
5217
    /**
5218
     * Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
5219
     *
5220
     * @param string $string Input string to en/decode: "mailto:[email protected]
5221
     * @param mixed  $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses
5222
     * @return string encoded version of $string
5223
     */
5224
    protected function encryptEmail(string $string, $type): string
5225
    {
5226
        $out = '';
5227
        // obfuscates using the decimal HTML entity references for each character
5228
        if ($type === 'ascii') {
5229
            foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5230
                $out .= '&#' . mb_ord($char) . ';';
5231
            }
5232
        } else {
5233
            // like str_rot13() but with a variable offset and a wider character range
5234
            $len = strlen($string);
5235
            $offset = (int)$type;
5236
            for ($i = 0; $i < $len; $i++) {
5237
                $charValue = ord($string[$i]);
5238
                // 0-9 . , - + / :
5239
                if ($charValue >= 43 && $charValue <= 58) {
5240
                    $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5241
                } elseif ($charValue >= 64 && $charValue <= 90) {
5242
                    // A-Z @
5243
                    $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5244
                } elseif ($charValue >= 97 && $charValue <= 122) {
5245
                    // a-z
5246
                    $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5247
                } else {
5248
                    $out .= $string[$i];
5249
                }
5250
            }
5251
        }
5252
        return $out;
5253
    }
5254
5255
    /**
5256
     * Encryption (or decryption) of a single character.
5257
     * Within the given range the character is shifted with the supplied offset.
5258
     *
5259
     * @param int $n Ordinal of input character
5260
     * @param int $start Start of range
5261
     * @param int $end End of range
5262
     * @param int $offset Offset
5263
     * @return string encoded/decoded version of character
5264
     */
5265
    protected function encryptCharcode($n, $start, $end, $offset)
5266
    {
5267
        $n = $n + $offset;
5268
        if ($offset > 0 && $n > $end) {
5269
            $n = $start + ($n - $end - 1);
5270
        } elseif ($offset < 0 && $n < $start) {
5271
            $n = $end - ($start - $n - 1);
5272
        }
5273
        return chr($n);
5274
    }
5275
5276
    /**
5277
     * Gets the query arguments and assembles them for URLs.
5278
     * Arguments may be removed or set, depending on configuration.
5279
     *
5280
     * @param array $conf Configuration
5281
     * @return string The URL query part (starting with a &)
5282
     */
5283
    public function getQueryArguments($conf)
5284
    {
5285
        $currentQueryArray = GeneralUtility::_GET();
5286
        if ($conf['exclude'] ?? false) {
5287
            $excludeString = str_replace(',', '&', $conf['exclude']);
5288
            $excludedQueryParts = [];
5289
            parse_str($excludeString, $excludedQueryParts);
5290
            $newQueryArray = ArrayUtility::arrayDiffKeyRecursive($currentQueryArray, $excludedQueryParts);
5291
        } else {
5292
            $newQueryArray = $currentQueryArray;
5293
        }
5294
        return HttpUtility::buildQueryString($newQueryArray, '&');
5295
    }
5296
5297
    /***********************************************
5298
     *
5299
     * Miscellaneous functions, stand alone
5300
     *
5301
     ***********************************************/
5302
    /**
5303
     * Wrapping a string.
5304
     * Implements the TypoScript "wrap" property.
5305
     * Example: $content = "HELLO WORLD" and $wrap = "<strong> | </strong>", result: "<strong>HELLO WORLD</strong>"
5306
     *
5307
     * @param string $content The content to wrap
5308
     * @param string $wrap The wrap value, eg. "<strong> | </strong>
5309
     * @param string $char The char used to split the wrapping value, default is "|
5310
     * @return string Wrapped input string
5311
     * @see noTrimWrap()
5312
     */
5313
    public function wrap($content, $wrap, $char = '|')
5314
    {
5315
        if ($wrap) {
5316
            $wrapArr = explode($char, $wrap);
5317
            $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
5318
        }
5319
        return $content;
5320
    }
5321
5322
    /**
5323
     * Wrapping a string, preserving whitespace in wrap value.
5324
     * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does)
5325
     *
5326
     * @param string $content The content to wrap, eg. "HELLO WORLD
5327
     * @param string $wrap The wrap value, eg. " | <strong> | </strong>
5328
     * @param string $char The char used to split the wrapping value, default is "|"
5329
     * @return string Wrapped input string, eg. " <strong> HELLO WORD </strong>
5330
     * @see wrap()
5331
     */
5332
    public function noTrimWrap($content, $wrap, $char = '|')
5333
    {
5334
        if ($wrap) {
5335
            // expects to be wrapped with (at least) 3 characters (before, middle, after)
5336
            // anything else is not taken into account
5337
            $wrapArr = explode($char, $wrap, 4);
5338
            $content = $wrapArr[1] . $content . $wrapArr[2];
5339
        }
5340
        return $content;
5341
    }
5342
5343
    /**
5344
     * Calling a user function/class-method
5345
     * Notice: For classes the instantiated object will have the internal variable, $cObj, set to be a *reference* to $this (the parent/calling object).
5346
     *
5347
     * @param string $funcName The functionname, eg "user_myfunction" or "user_myclass->main". Notice that there are rules for the names of functions/classes you can instantiate. If a function cannot be called for some reason it will be seen in the TypoScript log in the AdminPanel.
5348
     * @param array $conf The TypoScript configuration to pass the function
5349
     * @param mixed $content The content payload to pass the function
5350
     * @return mixed The return content from the function call. Should probably be a string.
5351
     * @see stdWrap()
5352
     * @see typoLink()
5353
     * @see _parseFunc()
5354
     */
5355
    public function callUserFunction($funcName, $conf, $content)
5356
    {
5357
        // Split parts
5358
        $parts = explode('->', $funcName);
5359
        if (count($parts) === 2) {
5360
            // Check whether PHP class is available
5361
            if (class_exists($parts[0])) {
5362
                if ($this->container && $this->container->has($parts[0])) {
5363
                    $classObj = $this->container->get($parts[0]);
5364
                } else {
5365
                    $classObj = GeneralUtility::makeInstance($parts[0]);
5366
                }
5367
                $methodName = (string)$parts[1];
5368
                $callable = [$classObj, $methodName];
5369
5370
                if (is_object($classObj) && method_exists($classObj, $parts[1]) && is_callable($callable)) {
5371
                    if (method_exists($classObj, 'setContentObjectRenderer') && is_callable([$classObj, 'setContentObjectRenderer'])) {
5372
                        $classObj->setContentObjectRenderer($this);
5373
                    } elseif (property_exists($classObj, 'cObj')) {
5374
                        trigger_error(
5375
                            'Setting public property "cObj" on "' . $parts[0] . '" is deprecated since v11 and will be removed in v12. Use explicit setter'
5376
                            . ' "public function setContentObjectRenderer(ContentObjectRenderer $cObj)" if your plugin needs an instance of ContentObjectRenderer instead.',
5377
                            E_USER_DEPRECATED
5378
                        );
5379
                        // Note this will still fatal if that property is protected. There is no way to
5380
                        // detect property visibility in PHP without reflection, so we'll deal with this in v11.
5381
                        // Extensions should either drop the property altogether if they don't need current instance
5382
                        // of ContentObjectRenderer, or set the property to protected and use the setter above.
5383
                        $classObj->cObj = $this;
5384
                    }
5385
                    $content = $callable($content, $conf, $this->getRequest());
5386
                } else {
5387
                    $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', LogLevel::ERROR);
5388
                }
5389
            } else {
5390
                $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', LogLevel::ERROR);
5391
            }
5392
        } elseif (function_exists($funcName)) {
5393
            $content = $funcName($content, $conf);
5394
        } else {
5395
            $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', LogLevel::ERROR);
5396
        }
5397
        return $content;
5398
    }
5399
5400
    /**
5401
     * Cleans up a string of keywords. Keywords at splitted by "," (comma)  ";" (semi colon) and linebreak
5402
     *
5403
     * @param string $content String of keywords
5404
     * @return string Cleaned up string, keywords will be separated by a comma only.
5405
     */
5406
    public function keywords($content)
5407
    {
5408
        $listArr = preg_split('/[,;' . LF . ']/', $content);
5409
        if ($listArr === false) {
5410
            return '';
5411
        }
5412
        foreach ($listArr as $k => $v) {
5413
            $listArr[$k] = trim($v);
5414
        }
5415
        return implode(',', $listArr);
5416
    }
5417
5418
    /**
5419
     * Changing character case of a string, converting typically used western charset characters as well.
5420
     *
5421
     * @param string $theValue The string to change case for.
5422
     * @param string $case The direction; either "upper" or "lower
5423
     * @return string
5424
     * @see HTMLcaseshift()
5425
     */
5426
    public function caseshift($theValue, $case)
5427
    {
5428
        switch (strtolower($case)) {
5429
            case 'upper':
5430
                $theValue = mb_strtoupper($theValue, 'utf-8');
5431
                break;
5432
            case 'lower':
5433
                $theValue = mb_strtolower($theValue, 'utf-8');
5434
                break;
5435
            case 'capitalize':
5436
                $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8');
5437
                break;
5438
            case 'ucfirst':
5439
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5440
                $firstChar = mb_strtoupper($firstChar, 'utf-8');
5441
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5442
                $theValue = $firstChar . $remainder;
5443
                break;
5444
            case 'lcfirst':
5445
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5446
                $firstChar = mb_strtolower($firstChar, 'utf-8');
5447
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5448
                $theValue = $firstChar . $remainder;
5449
                break;
5450
            case 'uppercamelcase':
5451
                $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue);
5452
                break;
5453
            case 'lowercamelcase':
5454
                $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue);
5455
                break;
5456
        }
5457
        return $theValue;
5458
    }
5459
5460
    /**
5461
     * Shifts the case of characters outside of HTML tags in the input string
5462
     *
5463
     * @param string $theValue The string to change case for.
5464
     * @param string $case The direction; either "upper" or "lower
5465
     * @return string
5466
     * @see caseshift()
5467
     */
5468
    public function HTMLcaseshift($theValue, $case)
5469
    {
5470
        $inside = 0;
5471
        $newVal = '';
5472
        $pointer = 0;
5473
        $totalLen = strlen($theValue);
5474
        do {
5475
            if (!$inside) {
5476
                $len = strcspn(substr($theValue, $pointer), '<');
5477
                $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case);
5478
                $inside = 1;
5479
            } else {
5480
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
5481
                $newVal .= substr($theValue, $pointer, $len);
5482
                $inside = 0;
5483
            }
5484
            $pointer += $len;
5485
        } while ($pointer < $totalLen);
5486
        return $newVal;
5487
    }
5488
5489
    /**
5490
     * Returns the 'age' of the tstamp $seconds
5491
     *
5492
     * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs
5493
     * @param string $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs'
5494
     * @return string The formatted string
5495
     */
5496
    public function calcAge($seconds, $labels)
5497
    {
5498
        if (MathUtility::canBeInterpretedAsInteger($labels)) {
5499
            $labels = ' min| hrs| days| yrs| min| hour| day| year';
5500
        } else {
5501
            $labels = str_replace('"', '', $labels);
5502
        }
5503
        $labelArr = explode('|', $labels);
5504
        if (count($labelArr) === 4) {
5505
            $labelArr = array_merge($labelArr, $labelArr);
5506
        }
5507
        $absSeconds = abs($seconds);
5508
        $sign = $seconds > 0 ? 1 : -1;
5509
        if ($absSeconds < 3600) {
5510
            $val = round($absSeconds / 60);
5511
            $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]);
5512
        } elseif ($absSeconds < 24 * 3600) {
5513
            $val = round($absSeconds / 3600);
5514
            $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]);
5515
        } elseif ($absSeconds < 365 * 24 * 3600) {
5516
            $val = round($absSeconds / (24 * 3600));
5517
            $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]);
5518
        } else {
5519
            $val = round($absSeconds / (365 * 24 * 3600));
5520
            $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null));
5521
        }
5522
        return $seconds;
5523
    }
5524
5525
    /**
5526
     * Resolves a TypoScript reference value to the full set of properties BUT overridden with any local properties set.
5527
     * So the reference is resolved but overlaid with local TypoScript properties of the reference value.
5528
     *
5529
     * @param array $confArr The TypoScript array
5530
     * @param string $prop The property name: If this value is a reference (eg. " < plugins.tx_something") then the reference will be retrieved and inserted at that position (into the properties only, not the value...) AND overlaid with the old properties if any.
5531
     * @return array The modified TypoScript array
5532
     */
5533
    public function mergeTSRef($confArr, $prop)
5534
    {
5535
        if ($confArr[$prop][0] === '<') {
5536
            $key = trim(substr($confArr[$prop], 1));
5537
            $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
5538
            // $name and $conf is loaded with the referenced values.
5539
            $old_conf = $confArr[$prop . '.'] ?? null;
5540
            $setupArray = [];
5541
            $tsfe = $this->getTypoScriptFrontendController();
5542
            if ($tsfe instanceof TypoScriptFrontendController
5543
                && $tsfe->tmpl instanceof TemplateService
5544
                && is_array($tsfe->tmpl->setup)
5545
            ) {
5546
                $setupArray = $tsfe->tmpl->setup;
5547
            }
5548
            $conf = $cF->getVal($key, $setupArray)[1];
5549
            if (is_array($old_conf) && !empty($old_conf)) {
5550
                $conf = array_replace_recursive($conf, $old_conf);
5551
            }
5552
            $confArr[$prop . '.'] = $conf;
5553
        }
5554
        return $confArr;
5555
    }
5556
5557
    /***********************************************
5558
     *
5559
     * Database functions, making of queries
5560
     *
5561
     ***********************************************/
5562
    /**
5563
     * Generates a list of Page-uid's from $id. List does not include $id itself
5564
     * (unless the id specified is negative in which case it does!)
5565
     * The only pages WHICH PREVENTS DECENDING in a branch are
5566
     * - deleted pages,
5567
     * - pages in a recycler (doktype = 255) or of the Backend User Section (doktpe = 6) type
5568
     * - pages that has the extendToSubpages set, WHERE start/endtime, hidden
5569
     * and fe_users would hide the records.
5570
     * Apart from that, pages with enable-fields excluding them, will also be
5571
     * removed. HOWEVER $dontCheckEnableFields set will allow
5572
     * enableFields-excluded pages to be included anyway - including
5573
     * extendToSubpages sections!
5574
     * Mount Pages are also descended but notice that these ID numbers are not
5575
     * useful for links unless the correct MPvar is set.
5576
     *
5577
     * @param int $id The id of the start page from which point in the page tree to descend. IF NEGATIVE the id itself is included in the end of the list (only if $begin is 0) AND the output does NOT contain a last comma. Recommended since it will resolve the input ID for mount pages correctly and also check if the start ID actually exists!
5578
     * @param int $depth The number of levels to descend. If you want to descend infinitely, just set this to 100 or so. Should be at least "1" since zero will just make the function return (no descend...)
5579
     * @param int $begin Is an optional integer that determines at which level in the tree to start collecting uid's. Zero means 'start right away', 1 = 'next level and out'
5580
     * @param bool $dontCheckEnableFields See function description
5581
     * @param string $addSelectFields Additional fields to select. Syntax: ",[fieldname],[fieldname],...
5582
     * @param string $moreWhereClauses Additional where clauses. Syntax: " AND [fieldname]=[value] AND ...
5583
     * @param array $prevId_array array of IDs from previous recursions. In order to prevent infinite loops with mount pages.
5584
     * @param int $recursionLevel Internal: Zero for the first recursion, incremented for each recursive call.
5585
     * @return string Returns the list of ids as a comma separated string
5586
     * @see TypoScriptFrontendController::checkEnableFields()
5587
     * @see TypoScriptFrontendController::checkPagerecordForIncludeSection()
5588
     */
5589
    public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0)
5590
    {
5591
        $id = (int)$id;
5592
        if (!$id) {
5593
            return '';
5594
        }
5595
5596
        // Init vars:
5597
        $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state,l10n_parent' . $addSelectFields;
5598
        $depth = (int)$depth;
5599
        $begin = (int)$begin;
5600
        $theList = [];
5601
        $addId = 0;
5602
        $requestHash = '';
5603
5604
        // First level, check id (second level, this is done BEFORE the recursive call)
5605
        $tsfe = $this->getTypoScriptFrontendController();
5606
        if (!$recursionLevel) {
5607
            // Check tree list cache
5608
            // First, create the hash for this request - not sure yet whether we need all these parameters though
5609
            $parameters = [
5610
                $id,
5611
                $depth,
5612
                $begin,
5613
                $dontCheckEnableFields,
5614
                $addSelectFields,
5615
                $moreWhereClauses,
5616
                $prevId_array,
5617
                GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1]),
5618
            ];
5619
            $requestHash = md5(serialize($parameters));
5620
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5621
                ->getQueryBuilderForTable('cache_treelist');
5622
            $cacheEntry = $queryBuilder->select('treelist')
5623
                ->from('cache_treelist')
5624
                ->where(
5625
                    $queryBuilder->expr()->eq(
5626
                        'md5hash',
5627
                        $queryBuilder->createNamedParameter($requestHash, \PDO::PARAM_STR)
5628
                    ),
5629
                    $queryBuilder->expr()->orX(
5630
                        $queryBuilder->expr()->gt(
5631
                            'expires',
5632
                            $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
5633
                        ),
5634
                        $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5635
                    )
5636
                )
5637
                ->setMaxResults(1)
5638
                ->execute()
5639
                ->fetchAssociative();
5640
5641
            if (is_array($cacheEntry)) {
5642
                // Cache hit
5643
                return $cacheEntry['treelist'];
5644
            }
5645
            // If Id less than zero it means we should add the real id to list:
5646
            if ($id < 0) {
5647
                $addId = $id = abs($id);
5648
            }
5649
            // Check start page:
5650
            if ($tsfe->sys_page->getRawRecord('pages', $id, 'uid')) {
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type double; however, parameter $uid of TYPO3\CMS\Core\Domain\Re...ository::getRawRecord() does only seem to accept integer, 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

5650
            if ($tsfe->sys_page->getRawRecord('pages', /** @scrutinizer ignore-type */ $id, 'uid')) {
Loading history...
5651
                // Find mount point if any:
5652
                $mount_info = $tsfe->sys_page->getMountPointInfo($id);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type double; however, parameter $pageId of TYPO3\CMS\Core\Domain\Re...ry::getMountPointInfo() does only seem to accept integer, 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

5652
                $mount_info = $tsfe->sys_page->getMountPointInfo(/** @scrutinizer ignore-type */ $id);
Loading history...
5653
                if (is_array($mount_info)) {
5654
                    $id = $mount_info['mount_pid'];
5655
                    // In Overlay mode, use the mounted page uid as added ID!:
5656
                    if ($addId && $mount_info['overlay']) {
5657
                        $addId = $id;
5658
                    }
5659
                }
5660
            } else {
5661
                // Return blank if the start page was NOT found at all!
5662
                return '';
5663
            }
5664
        }
5665
        // Add this ID to the array of IDs
5666
        if ($begin <= 0) {
5667
            $prevId_array[] = $id;
5668
        }
5669
        // Select sublevel:
5670
        if ($depth > 0) {
5671
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
5672
            $queryBuilder->getRestrictions()
5673
                ->removeAll()
5674
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5675
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5676
                ->from('pages')
5677
                ->where(
5678
                    $queryBuilder->expr()->eq(
5679
                        'pid',
5680
                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
5681
                    ),
5682
                    // tree is only built by language=0 pages
5683
                    $queryBuilder->expr()->eq('sys_language_uid', 0)
5684
                )
5685
                ->orderBy('sorting');
5686
5687
            if (!empty($moreWhereClauses)) {
5688
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5689
            }
5690
5691
            $result = $queryBuilder->execute();
5692
            while ($row = $result->fetchAssociative()) {
5693
                /** @var VersionState $versionState */
5694
                $versionState = VersionState::cast($row['t3ver_state']);
5695
                $tsfe->sys_page->versionOL('pages', $row);
5696
                if ($row === false
5697
                    || (int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5698
                    || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5699
                    || $versionState->indicatesPlaceholder()
5700
                ) {
5701
                    // falsy row means Overlay prevents access to this page.
5702
                    // Doing this after the overlay to make sure changes
5703
                    // in the overlay are respected.
5704
                    // However, we do not process pages below of and
5705
                    // including of type recycler and BE user section
5706
                    continue;
5707
                }
5708
                // Find mount point if any:
5709
                $next_id = $row['uid'];
5710
                $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row);
5711
                // Overlay mode:
5712
                if (is_array($mount_info) && $mount_info['overlay']) {
5713
                    $next_id = $mount_info['mount_pid'];
5714
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5715
                        ->getQueryBuilderForTable('pages');
5716
                    $queryBuilder->getRestrictions()
5717
                        ->removeAll()
5718
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5719
                    $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5720
                        ->from('pages')
5721
                        ->where(
5722
                            $queryBuilder->expr()->eq(
5723
                                'uid',
5724
                                $queryBuilder->createNamedParameter($next_id, \PDO::PARAM_INT)
5725
                            )
5726
                        )
5727
                        ->orderBy('sorting')
5728
                        ->setMaxResults(1);
5729
5730
                    if (!empty($moreWhereClauses)) {
5731
                        $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5732
                    }
5733
5734
                    $row = $queryBuilder->execute()->fetchAssociative();
5735
                    $tsfe->sys_page->versionOL('pages', $row);
5736
                    if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5737
                        || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5738
                        || $versionState->indicatesPlaceholder()
5739
                    ) {
5740
                        // Doing this after the overlay to make sure
5741
                        // changes in the overlay are respected.
5742
                        // see above
5743
                        continue;
5744
                    }
5745
                }
5746
                // Add record:
5747
                if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) {
5748
                    // Add ID to list:
5749
                    if ($begin <= 0) {
5750
                        if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) {
5751
                            $theList[] = $next_id;
5752
                        }
5753
                    }
5754
                    // Next level:
5755
                    if ($depth > 1 && !$row['php_tree_stop']) {
5756
                        // Normal mode:
5757
                        if (is_array($mount_info) && !$mount_info['overlay']) {
5758
                            $next_id = $mount_info['mount_pid'];
5759
                        }
5760
                        // Call recursively, if the id is not in prevID_array:
5761
                        if (!in_array($next_id, $prevId_array)) {
5762
                            $theList = array_merge(
5763
                                GeneralUtility::intExplode(
5764
                                    ',',
5765
                                    $this->getTreeList(
5766
                                        $next_id,
5767
                                        $depth - 1,
5768
                                        $begin - 1,
5769
                                        $dontCheckEnableFields,
5770
                                        $addSelectFields,
5771
                                        $moreWhereClauses,
5772
                                        $prevId_array,
5773
                                        $recursionLevel + 1
5774
                                    ),
5775
                                    true
5776
                                ),
5777
                                $theList
5778
                            );
5779
                        }
5780
                    }
5781
                }
5782
            }
5783
        }
5784
        // If first run, check if the ID should be returned:
5785
        if (!$recursionLevel) {
5786
            if ($addId) {
5787
                if ($begin > 0) {
5788
                    $theList[] = 0;
5789
                } else {
5790
                    $theList[] = $addId;
5791
                }
5792
            }
5793
5794
            $cacheEntry = [
5795
                'md5hash' => $requestHash,
5796
                'pid' => $id,
5797
                'treelist' => implode(',', $theList),
5798
                'tstamp' => $GLOBALS['EXEC_TIME'],
5799
            ];
5800
5801
            // Only add to cache if not logged into TYPO3 Backend
5802
            if (!$this->getFrontendBackendUser() instanceof AbstractUserAuthentication) {
0 ignored issues
show
introduced by
$this->getFrontendBackendUser() is always a sub-type of TYPO3\CMS\Core\Authentic...tractUserAuthentication.
Loading history...
5803
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
5804
                try {
5805
                    $connection->transactional(static function ($connection) use ($cacheEntry) {
5806
                        $connection->insert('cache_treelist', $cacheEntry);
5807
                    });
5808
                } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
5809
                }
5810
            }
5811
        }
5812
5813
        return implode(',', $theList);
5814
    }
5815
5816
    /**
5817
     * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.)
5818
     * Example: The $sw is "content management, system" (from an input form) and the $searchFieldList is "bodytext,header" then the output will be ' AND (bodytext LIKE "%content%" OR header LIKE "%content%") AND (bodytext LIKE "%management%" OR header LIKE "%management%") AND (bodytext LIKE "%system%" OR header LIKE "%system%")'
5819
     *
5820
     * @param string $searchWords The search words. These will be separated by space and comma.
5821
     * @param string $searchFieldList The fields to search in
5822
     * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well)
5823
     * @return string The WHERE clause.
5824
     */
5825
    public function searchWhere($searchWords, $searchFieldList, $searchTable)
5826
    {
5827
        if (!$searchWords) {
5828
            return '';
5829
        }
5830
5831
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5832
            ->getQueryBuilderForTable($searchTable);
5833
5834
        $prefixTableName = $searchTable ? $searchTable . '.' : '';
5835
5836
        $where = $queryBuilder->expr()->andX();
5837
        $searchFields = explode(',', $searchFieldList);
5838
        $searchWords = preg_split('/[ ,]/', $searchWords);
5839
        foreach ($searchWords as $searchWord) {
5840
            $searchWord = trim($searchWord);
5841
            if (strlen($searchWord) < 3) {
5842
                continue;
5843
            }
5844
            $searchWordConstraint = $queryBuilder->expr()->orX();
5845
            $searchWord = $queryBuilder->escapeLikeWildcards($searchWord);
5846
            foreach ($searchFields as $field) {
5847
                $searchWordConstraint->add(
5848
                    $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%'))
5849
                );
5850
            }
5851
5852
            if ($searchWordConstraint->count()) {
5853
                $where->add($searchWordConstraint);
5854
            }
5855
        }
5856
5857
        if ((string)$where === '') {
5858
            return '';
5859
        }
5860
5861
        return ' AND (' . (string)$where . ')';
5862
    }
5863
5864
    /**
5865
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5866
     * This function is preferred over ->getQuery() if you just need to create and then execute a query.
5867
     *
5868
     * @param string $table The table name
5869
     * @param array $conf The TypoScript configuration properties
5870
     * @return Statement
5871
     * @see getQuery()
5872
     */
5873
    public function exec_getQuery($table, $conf)
5874
    {
5875
        $statement = $this->getQuery($table, $conf);
5876
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5877
5878
        return $connection->executeQuery($statement);
5879
    }
5880
5881
    /**
5882
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5883
     * and overlays with translation and version if available
5884
     *
5885
     * @param string $tableName the name of the TCA database table
5886
     * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference
5887
     * @return array The records
5888
     * @throws \UnexpectedValueException
5889
     */
5890
    public function getRecords($tableName, array $queryConfiguration)
5891
    {
5892
        $records = [];
5893
5894
        $statement = $this->exec_getQuery($tableName, $queryConfiguration);
5895
5896
        $tsfe = $this->getTypoScriptFrontendController();
5897
        while ($row = $statement->fetchAssociative()) {
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Statement::fetchAssociative() has been deprecated: Use Result::fetchAssociative() instead ( Ignorable by Annotation )

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

5897
        while ($row = /** @scrutinizer ignore-deprecated */ $statement->fetchAssociative()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
5898
            // Versioning preview:
5899
            $tsfe->sys_page->versionOL($tableName, $row, true);
5900
5901
            // Language overlay:
5902
            if (is_array($row)) {
5903
                $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row);
5904
            }
5905
5906
            // Might be unset in the language overlay
5907
            if (is_array($row)) {
5908
                $records[] = $row;
5909
            }
5910
        }
5911
5912
        return $records;
5913
    }
5914
5915
    /**
5916
     * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5917
     * Implements the "select" function in TypoScript
5918
     *
5919
     * @param string $table See ->exec_getQuery()
5920
     * @param array $conf See ->exec_getQuery()
5921
     * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED!
5922
     * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts.
5923
     * @throws \RuntimeException
5924
     * @throws \InvalidArgumentException
5925
     * @internal
5926
     * @see numRows()
5927
     */
5928
    public function getQuery($table, $conf, $returnQueryArray = false)
5929
    {
5930
        // Resolve stdWrap in these properties first
5931
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5932
        $properties = [
5933
            'pidInList',
5934
            'uidInList',
5935
            'languageField',
5936
            'selectFields',
5937
            'max',
5938
            'begin',
5939
            'groupBy',
5940
            'orderBy',
5941
            'join',
5942
            'leftjoin',
5943
            'rightjoin',
5944
            'recursive',
5945
            'where',
5946
        ];
5947
        foreach ($properties as $property) {
5948
            $conf[$property] = trim(
5949
                isset($conf[$property . '.'])
5950
                    ? $this->stdWrap($conf[$property] ?? '', $conf[$property . '.'] ?? [])
5951
                    : ($conf[$property] ?? '')
5952
            );
5953
            if ($conf[$property] === '') {
5954
                unset($conf[$property]);
5955
            } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) {
5956
                $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]);
5957
            }
5958
            if (isset($conf[$property . '.'])) {
5959
                // stdWrapping already done, so remove the sub-array
5960
                unset($conf[$property . '.']);
5961
            }
5962
        }
5963
        // Handle PDO-style named parameter markers first
5964
        $queryMarkers = $this->getQueryMarkers($table, $conf);
5965
        // Replace the markers in the non-stdWrap properties
5966
        foreach ($queryMarkers as $marker => $markerValue) {
5967
            $properties = [
5968
                'uidInList',
5969
                'selectFields',
5970
                'where',
5971
                'max',
5972
                'begin',
5973
                'groupBy',
5974
                'orderBy',
5975
                'join',
5976
                'leftjoin',
5977
                'rightjoin',
5978
            ];
5979
            foreach ($properties as $property) {
5980
                if ($conf[$property] ?? false) {
5981
                    $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]);
5982
                }
5983
            }
5984
        }
5985
5986
        // Construct WHERE clause:
5987
        // Handle recursive function for the pidInList
5988
        if (isset($conf['recursive'])) {
5989
            $conf['recursive'] = (int)$conf['recursive'];
5990
            if ($conf['recursive'] > 0) {
5991
                $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true);
5992
                array_walk($pidList, function (&$storagePid) {
5993
                    if ($storagePid === 'this') {
5994
                        $storagePid = $this->getTypoScriptFrontendController()->id;
5995
                    }
5996
                    if ($storagePid > 0) {
5997
                        $storagePid = -$storagePid;
5998
                    }
5999
                });
6000
                $expandedPidList = [];
6001
                foreach ($pidList as $value) {
6002
                    // Implementation of getTreeList allows to pass the id negative to include
6003
                    // it into the result otherwise only childpages are returned
6004
                    $expandedPidList = array_merge(
6005
                        GeneralUtility::intExplode(',', $this->getTreeList((int)$value, (int)($conf['recursive'] ?? 0))),
6006
                        $expandedPidList
6007
                    );
6008
                }
6009
                $conf['pidInList'] = implode(',', $expandedPidList);
6010
            }
6011
        }
6012
        if ((string)($conf['pidInList'] ?? '') === '') {
6013
            $conf['pidInList'] = 'this';
6014
        }
6015
6016
        $queryParts = $this->getQueryConstraints($table, $conf);
6017
6018
        $queryBuilder = $connection->createQueryBuilder();
6019
        // @todo Check against getQueryConstraints, can probably use FrontendRestrictions
6020
        // @todo here and remove enableFields there.
6021
        $queryBuilder->getRestrictions()->removeAll();
6022
        $queryBuilder->select('*')->from($table);
6023
6024
        if ($queryParts['where'] ?? false) {
6025
            $queryBuilder->where($queryParts['where']);
6026
        }
6027
6028
        if ($queryParts['groupBy'] ?? false) {
6029
            $queryBuilder->groupBy(...$queryParts['groupBy']);
6030
        }
6031
6032
        if (is_array($queryParts['orderBy'] ?? false)) {
6033
            foreach ($queryParts['orderBy'] as $orderBy) {
6034
                $queryBuilder->addOrderBy(...$orderBy);
6035
            }
6036
        }
6037
6038
        // Fields:
6039
        if ($conf['selectFields'] ?? false) {
6040
            $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table));
6041
        }
6042
6043
        // Setting LIMIT:
6044
        $error = false;
6045
        if (($conf['max'] ?? false) || ($conf['begin'] ?? false)) {
6046
            // Finding the total number of records, if used:
6047
            if (str_contains(strtolower(($conf['begin'] ?? '') . $conf['max']), 'total')) {
6048
                $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6049
                $countQueryBuilder->getRestrictions()->removeAll();
6050
                $countQueryBuilder->count('*')
6051
                    ->from($table)
6052
                    ->where($queryParts['where']);
6053
6054
                if ($queryParts['groupBy']) {
6055
                    $countQueryBuilder->groupBy(...$queryParts['groupBy']);
6056
                }
6057
6058
                try {
6059
                    $count = $countQueryBuilder->execute()->fetchOne();
6060
                    $conf['max'] = str_ireplace('total', $count, $conf['max']);
6061
                    $conf['begin'] = str_ireplace('total', $count, $conf['begin']);
6062
                } catch (DBALException $e) {
6063
                    $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage());
6064
                    $error = true;
6065
                }
6066
            }
6067
6068
            if (!$error) {
6069
                $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'] ?? '')), 0);
6070
                $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'] ?? '')), 0);
6071
                if ($conf['begin'] > 0) {
6072
                    $queryBuilder->setFirstResult($conf['begin']);
6073
                }
6074
                $queryBuilder->setMaxResults($conf['max'] ?: 100000);
6075
            }
6076
        }
6077
6078
        if (!$error) {
6079
            // Setting up tablejoins:
6080
            if ($conf['join'] ?? false) {
6081
                $joinParts = QueryHelper::parseJoin($conf['join']);
6082
                $queryBuilder->join(
6083
                    $table,
6084
                    $joinParts['tableName'],
6085
                    $joinParts['tableAlias'],
6086
                    $joinParts['joinCondition']
6087
                );
6088
            } elseif ($conf['leftjoin'] ?? false) {
6089
                $joinParts = QueryHelper::parseJoin($conf['leftjoin']);
6090
                $queryBuilder->leftJoin(
6091
                    $table,
6092
                    $joinParts['tableName'],
6093
                    $joinParts['tableAlias'],
6094
                    $joinParts['joinCondition']
6095
                );
6096
            } elseif ($conf['rightjoin'] ?? false) {
6097
                $joinParts = QueryHelper::parseJoin($conf['rightjoin']);
6098
                $queryBuilder->rightJoin(
6099
                    $table,
6100
                    $joinParts['tableName'],
6101
                    $joinParts['tableAlias'],
6102
                    $joinParts['joinCondition']
6103
                );
6104
            }
6105
6106
            // Convert the QueryBuilder object into a SQL statement.
6107
            $query = $queryBuilder->getSQL();
6108
6109
            // Replace the markers in the queryParts to handle stdWrap enabled properties
6110
            foreach ($queryMarkers as $marker => $markerValue) {
6111
                // @todo Ugly hack that needs to be cleaned up, with the current architecture
6112
                // @todo for exec_Query / getQuery it's the best we can do.
6113
                $query = str_replace('###' . $marker . '###', $markerValue, $query);
6114
            }
6115
6116
            return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
6117
        }
6118
6119
        return '';
6120
    }
6121
6122
    /**
6123
     * Helper to transform a QueryBuilder object into a queryParts array that can be used
6124
     * with exec_SELECT_queryArray
6125
     *
6126
     * @param \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder
6127
     * @return array
6128
     * @throws \RuntimeException
6129
     */
6130
    protected function getQueryArray(QueryBuilder $queryBuilder)
6131
    {
6132
        $fromClauses = [];
6133
        $knownAliases = [];
6134
        $queryParts = [];
6135
6136
        // Loop through all FROM clauses
6137
        foreach ($queryBuilder->getQueryPart('from') as $from) {
6138
            if ($from['alias'] === null) {
6139
                $tableSql = $from['table'];
6140
                $tableReference = $from['table'];
6141
            } else {
6142
                $tableSql = $from['table'] . ' ' . $from['alias'];
6143
                $tableReference = $from['alias'];
6144
            }
6145
6146
            $knownAliases[$tableReference] = true;
6147
6148
            $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
6149
                $tableReference,
6150
                $queryBuilder->getQueryPart('join'),
6151
                $knownAliases
6152
            );
6153
        }
6154
6155
        $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
6156
        $queryParts['FROM'] = implode(', ', $fromClauses);
6157
        $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
6158
        $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
6159
        $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
6160
        if ($queryBuilder->getFirstResult() > 0) {
6161
            $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
6162
        } elseif ($queryBuilder->getMaxResults() > 0) {
6163
            $queryParts['LIMIT'] = $queryBuilder->getMaxResults();
6164
        }
6165
6166
        return $queryParts;
6167
    }
6168
6169
    /**
6170
     * Helper to transform the QueryBuilder join part into a SQL fragment.
6171
     *
6172
     * @param string $fromAlias
6173
     * @param array $joinParts
6174
     * @param array $knownAliases
6175
     * @return string
6176
     * @throws \RuntimeException
6177
     */
6178
    protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
6179
    {
6180
        $sql = '';
6181
6182
        if (isset($joinParts['join'][$fromAlias])) {
6183
            foreach ($joinParts['join'][$fromAlias] as $join) {
6184
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
6185
                    throw new \RuntimeException(
6186
                        'Non unique join alias: "' . $join['joinAlias'] . '" found.',
6187
                        1472748872
6188
                    );
6189
                }
6190
                $sql .= ' ' . strtoupper($join['joinType'])
6191
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
6192
                    . ' ON ' . ((string)$join['joinCondition']);
6193
                $knownAliases[$join['joinAlias']] = true;
6194
            }
6195
6196
            foreach ($joinParts['join'][$fromAlias] as $join) {
6197
                $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
6198
            }
6199
        }
6200
6201
        return $sql;
6202
    }
6203
    /**
6204
     * Helper function for getQuery(), creating the WHERE clause of the SELECT query
6205
     *
6206
     * @param string $table The table name
6207
     * @param array $conf The TypoScript configuration properties
6208
     * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments
6209
     * @throws \InvalidArgumentException
6210
     * @see getQuery()
6211
     */
6212
    protected function getQueryConstraints(string $table, array $conf): array
6213
    {
6214
        // Init:
6215
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6216
        $expressionBuilder = $queryBuilder->expr();
6217
        $tsfe = $this->getTypoScriptFrontendController();
6218
        $constraints = [];
6219
        $pid_uid_flag = 0;
6220
        $enableFieldsIgnore = [];
6221
        $queryParts = [
6222
            'where' => null,
6223
            'groupBy' => null,
6224
            'orderBy' => null,
6225
        ];
6226
6227
        $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
6228
        $considerMovePointers = (
6229
            $isInWorkspace && $table !== 'pages'
6230
            && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
6231
        );
6232
6233
        if (trim($conf['uidInList'] ?? '')) {
6234
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['uidInList']));
6235
6236
            // If moved records shall be considered, select via t3ver_oid
6237
            if ($considerMovePointers) {
6238
                $constraints[] = (string)$expressionBuilder->orX(
6239
                    $expressionBuilder->in($table . '.uid', $listArr),
6240
                    $expressionBuilder->andX(
6241
                        $expressionBuilder->eq(
6242
                            $table . '.t3ver_state',
6243
                            (int)(string)VersionState::cast(VersionState::MOVE_POINTER)
6244
                        ),
6245
                        $expressionBuilder->in($table . '.t3ver_oid', $listArr)
6246
                    )
6247
                );
6248
            } else {
6249
                $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr);
6250
            }
6251
            $pid_uid_flag++;
6252
        }
6253
6254
        // Static_* tables are allowed to be fetched from root page
6255
        if (strpos($table, 'static_') === 0) {
6256
            $pid_uid_flag++;
6257
        }
6258
6259
        if (trim($conf['pidInList'])) {
6260
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['pidInList']));
6261
            // Removes all pages which are not visible for the user!
6262
            $listArr = $this->checkPidArray($listArr);
6263
            if (GeneralUtility::inList($conf['pidInList'], 'root')) {
6264
                $listArr[] = 0;
6265
            }
6266
            if (GeneralUtility::inList($conf['pidInList'], '-1')) {
6267
                $listArr[] = -1;
6268
                $enableFieldsIgnore['pid'] = true;
6269
            }
6270
            if (!empty($listArr)) {
6271
                $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr));
6272
                $pid_uid_flag++;
6273
            } else {
6274
                // If not uid and not pid then uid is set to 0 - which results in nothing!!
6275
                $pid_uid_flag = 0;
6276
            }
6277
        }
6278
6279
        // If not uid and not pid then uid is set to 0 - which results in nothing!!
6280
        if (!$pid_uid_flag) {
6281
            $constraints[] = $expressionBuilder->eq($table . '.uid', 0);
6282
        }
6283
6284
        $where = trim((string)$this->stdWrapValue('where', $conf ?? []));
6285
        if ($where) {
6286
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
6287
        }
6288
6289
        // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched
6290
        // but only do this for TCA tables that have languages enabled
6291
        $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class));
6292
        if ($languageConstraint !== null) {
6293
            $constraints[] = $languageConstraint;
6294
        }
6295
6296
        // Enablefields
6297
        if ($table === 'pages') {
6298
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
6299
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
6300
        } else {
6301
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
6302
        }
6303
6304
        // MAKE WHERE:
6305
        if (count($constraints) !== 0) {
6306
            $queryParts['where'] = $expressionBuilder->andX(...$constraints);
6307
        }
6308
        // GROUP BY
6309
        $groupBy = trim((string)$this->stdWrapValue('groupBy', $conf ?? []));
6310
        if ($groupBy) {
6311
            $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy);
6312
        }
6313
6314
        // ORDER BY
6315
        $orderByString = trim((string)$this->stdWrapValue('orderBy', $conf ?? []));
6316
        if ($orderByString) {
6317
            $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString);
6318
        }
6319
6320
        // Return result:
6321
        return $queryParts;
6322
    }
6323
6324
    /**
6325
     * Adds parts to the WHERE clause that are related to language.
6326
     * This only works on TCA tables which have the [ctrl][languageField] field set or if they
6327
     * have select.languageField = my_language_field set explicitly.
6328
     *
6329
     * It is also possible to disable the language restriction for a query by using select.languageField = 0,
6330
     * if select.languageField is not explicitly set, the TCA default values are taken.
6331
     *
6332
     * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted:
6333
     *
6334
     * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are
6335
     * fetched (the overlays are taken care of later-on).
6336
     * if the current language has overlays but also records without localization-parent (free mode) available,
6337
     * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1
6338
     * which overrules the overlayType within the language aspect.
6339
     *
6340
     * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records
6341
     * for the current language.
6342
     *
6343
     * @param ExpressionBuilder $expressionBuilder
6344
     * @param string $table
6345
     * @param array $conf
6346
     * @param Context $context
6347
     * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null
6348
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
6349
     */
6350
    protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
6351
    {
6352
        $languageField = '';
6353
        $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
6354
        // Check if the table is translatable, and set the language field by default from the TCA information
6355
        if (!empty($conf['languageField']) || !isset($conf['languageField'])) {
6356
            if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) {
6357
                $languageField = $conf['languageField'];
6358
            } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) {
6359
                $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
6360
            }
6361
        }
6362
6363
        // No language restriction enabled explicitly or available via TCA
6364
        if (empty($languageField)) {
6365
            return null;
6366
        }
6367
6368
        /** @var LanguageAspect $languageAspect */
6369
        $languageAspect = $context->getAspect('language');
6370
        if ($languageAspect->doOverlays() && !empty($localizationParentField)) {
6371
            // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will
6372
            // OVERLAY the records with localized versions!
6373
            $languageQuery = $expressionBuilder->in($languageField, [0, -1]);
6374
            // Use this option to include records that don't have a default language counterpart ("free mode")
6375
            // (originalpointerfield is 0 and the language field contains the requested language)
6376
            if (isset($conf['includeRecordsWithoutDefaultTranslation']) || !empty($conf['includeRecordsWithoutDefaultTranslation.'])) {
6377
                $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.'])
6378
                    ? $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.'])
6379
                    : $conf['includeRecordsWithoutDefaultTranslation'];
6380
                $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== '';
6381
            } else {
6382
                // Option was not explicitly set, check what's in for the language overlay type.
6383
                $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING;
6384
            }
6385
            if ($includeRecordsWithoutDefaultTranslation) {
6386
                $languageQuery = $expressionBuilder->orX(
6387
                    $languageQuery,
6388
                    $expressionBuilder->andX(
6389
                        $expressionBuilder->eq($table . '.' . $localizationParentField, 0),
6390
                        $expressionBuilder->eq($languageField, $languageAspect->getContentId())
6391
                    )
6392
                );
6393
            }
6394
            return $languageQuery;
6395
        }
6396
        // No overlays = only fetch records given for the requested language and "all languages"
6397
        return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]);
6398
    }
6399
6400
    /**
6401
     * Helper function for getQuery, sanitizing the select part
6402
     *
6403
     * This functions checks if the necessary fields are part of the select
6404
     * and adds them if necessary.
6405
     *
6406
     * @param string $selectPart Select part
6407
     * @param string $table Table to select from
6408
     * @return string Sanitized select part
6409
     * @internal
6410
     * @see getQuery
6411
     */
6412
    protected function sanitizeSelectPart($selectPart, $table)
6413
    {
6414
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6415
6416
        // Pattern matching parts
6417
        $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
6418
        $matchEnd = '(\\s*,|\\s*$)/';
6419
        $necessaryFields = ['uid', 'pid'];
6420
        $wsFields = ['t3ver_state'];
6421
        if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) {
6422
            foreach ($necessaryFields as $field) {
6423
                $match = $matchStart . $field . $matchEnd;
6424
                if (!preg_match($match, $selectPart)) {
6425
                    $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6426
                }
6427
            }
6428
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
6429
                foreach ($wsFields as $field) {
6430
                    $match = $matchStart . $field . $matchEnd;
6431
                    if (!preg_match($match, $selectPart)) {
6432
                        $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6433
                    }
6434
                }
6435
            }
6436
        }
6437
        return $selectPart;
6438
    }
6439
6440
    /**
6441
     * Removes Page UID numbers from the input array which are not available due to enableFields() or the list of bad doktype numbers ($this->checkPid_badDoktypeList)
6442
     *
6443
     * @param int[] $pageIds Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed.
6444
     * @return array Returns the array of remaining page UID numbers
6445
     * @internal
6446
     */
6447
    public function checkPidArray($pageIds)
6448
    {
6449
        if (!is_array($pageIds) || empty($pageIds)) {
0 ignored issues
show
introduced by
The condition is_array($pageIds) is always true.
Loading history...
6450
            return [];
6451
        }
6452
        $restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class);
6453
        $restrictionContainer->add(GeneralUtility::makeInstance(
6454
            DocumentTypeExclusionRestriction::class,
6455
            GeneralUtility::intExplode(',', (string)$this->checkPid_badDoktypeList, true)
6456
        ));
6457
        return $this->getTypoScriptFrontendController()->sys_page->filterAccessiblePageIds($pageIds, $restrictionContainer);
6458
    }
6459
6460
    /**
6461
     * Builds list of marker values for handling PDO-like parameter markers in select parts.
6462
     * Marker values support stdWrap functionality thus allowing a way to use stdWrap functionality in various properties of 'select' AND prevents SQL-injection problems by quoting and escaping of numeric values, strings, NULL values and comma separated lists.
6463
     *
6464
     * @param string $table Table to select records from
6465
     * @param array $conf Select part of CONTENT definition
6466
     * @return array List of values to replace markers with
6467
     * @internal
6468
     * @see getQuery()
6469
     */
6470
    public function getQueryMarkers($table, $conf)
6471
    {
6472
        if (!isset($conf['markers.']) || !is_array($conf['markers.'])) {
6473
            return [];
6474
        }
6475
        // Parse markers and prepare their values
6476
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6477
        $markerValues = [];
6478
        foreach ($conf['markers.'] as $dottedMarker => $dummy) {
6479
            $marker = rtrim($dottedMarker, '.');
6480
            if ($dottedMarker != $marker . '.') {
6481
                continue;
6482
            }
6483
            // Parse definition
6484
            // todo else value is always null
6485
            $tempValue = isset($conf['markers.'][$dottedMarker])
6486
                ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'] ?? '', $conf['markers.'][$dottedMarker])
6487
                : $conf['markers.'][$dottedMarker]['value'];
6488
            // Quote/escape if needed
6489
            if (is_numeric($tempValue)) {
6490
                if ((int)$tempValue == $tempValue) {
6491
                    // Handle integer
6492
                    $markerValues[$marker] = (int)$tempValue;
6493
                } else {
6494
                    // Handle float
6495
                    $markerValues[$marker] = (float)$tempValue;
6496
                }
6497
            } elseif ($tempValue === null) {
6498
                // It represents NULL
6499
                $markerValues[$marker] = 'NULL';
6500
            } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
6501
                // See if it is really a comma separated list of values
6502
                $explodeValues = GeneralUtility::trimExplode(',', $tempValue);
6503
                if (count($explodeValues) > 1) {
6504
                    // Handle each element of list separately
6505
                    $tempArray = [];
6506
                    foreach ($explodeValues as $listValue) {
6507
                        if (is_numeric($listValue)) {
6508
                            if ((int)$listValue == $listValue) {
6509
                                $tempArray[] = (int)$listValue;
6510
                            } else {
6511
                                $tempArray[] = (float)$listValue;
6512
                            }
6513
                        } else {
6514
                            // If quoted, remove quotes before
6515
                            // escaping.
6516
                            if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
6517
                                $listValue = $matches[1];
6518
                            } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
6519
                                $listValue = $matches[1];
6520
                            }
6521
                            $tempArray[] = $connection->quote($listValue);
6522
                        }
6523
                    }
6524
                    $markerValues[$marker] = implode(',', $tempArray);
6525
                } else {
6526
                    // Handle remaining values as string
6527
                    $markerValues[$marker] = $connection->quote($tempValue);
6528
                }
6529
            } else {
6530
                // Handle remaining values as string
6531
                $markerValues[$marker] = $connection->quote($tempValue);
6532
            }
6533
        }
6534
        return $markerValues;
6535
    }
6536
6537
    /***********************************************
6538
     *
6539
     * Frontend editing functions
6540
     *
6541
     ***********************************************/
6542
    /**
6543
     * Generates the "edit panels" which can be shown for a page or records on a page when the Admin Panel is enabled for a backend users surfing the frontend.
6544
     * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
6545
     * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
6546
     *
6547
     * @param string $content A content string containing the content related to the edit panel. For cObject "EDITPANEL" this is empty but not so for the stdWrap property. The edit panel is appended to this string and returned.
6548
     * @param array $conf TypoScript configuration properties for the editPanel
6549
     * @param string $currentRecord The "table:uid" of the record being shown. If empty string then $this->currentRecord is used. For new records (set by $conf['newRecordFromTable']) it's auto-generated to "[tablename]:NEW
6550
     * @param array $dataArray Alternative data array to use. Default is $this->data
6551
     * @return string The input content string with the editPanel appended. This function returns only an edit panel appended to the content string if a backend user is logged in (and has the correct permissions). Otherwise the content string is directly returned.
6552
     * @deprecated since v11, will be removed with v12. Drop together with other editPanel removals.
6553
     */
6554
    public function editPanel($content, $conf, $currentRecord = '', $dataArray = [])
6555
    {
6556
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6557
            return $content;
6558
        }
6559
        if (!$this->getTypoScriptFrontendController()->displayEditIcons) {
6560
            return $content;
6561
        }
6562
6563
        if (!$currentRecord) {
6564
            $currentRecord = $this->currentRecord;
6565
        }
6566
        if (empty($dataArray)) {
6567
            $dataArray = $this->data;
6568
        }
6569
6570
        if ($conf['newRecordFromTable']) {
6571
            $currentRecord = $conf['newRecordFromTable'] . ':NEW';
6572
            $conf['allow'] = 'new';
6573
            $checkEditAccessInternals = false;
6574
        } else {
6575
            $checkEditAccessInternals = true;
6576
        }
6577
        [$table, $uid] = explode(':', $currentRecord);
6578
        // Page ID for new records, 0 if not specified
6579
        $newRecordPid = (int)$conf['newRecordInPid'];
6580
        $newUid = null;
6581
        if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) {
6582
            if ($table === 'pages') {
6583
                $newUid = $uid;
6584
            } else {
6585
                if ($conf['newRecordFromTable']) {
6586
                    $newUid = $this->getTypoScriptFrontendController()->id;
6587
                    if ($newRecordPid) {
6588
                        $newUid = $newRecordPid;
6589
                    }
6590
                } else {
6591
                    $newUid = -1 * $uid;
6592
                }
6593
            }
6594
        }
6595
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6596
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6597
            if ($editClass) {
6598
                trigger_error('Hook "typo3/classes/class.frontendedit.php" is deprecated together with stdWrap.editPanel and will be removed in TYPO3 12.0.', E_USER_DEPRECATED);
6599
                $edit = GeneralUtility::makeInstance($editClass);
6600
                $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']);
6601
                $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []);
6602
            }
6603
        }
6604
        return $content;
6605
    }
6606
6607
    /**
6608
     * Adds an edit icon to the content string. The edit icon links to FormEngine with proper parameters for editing the table/fields of the context.
6609
     * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
6610
     *
6611
     * @param string $content The content to which the edit icons should be appended
6612
     * @param string $params The parameters defining which table and fields to edit. Syntax is [tablename]:[fieldname],[fieldname],[fieldname],... OR [fieldname],[fieldname],[fieldname],... (basically "[tablename]:" is optional, default table is the one of the "current record" used in the function). The fieldlist is sent as "&columnsOnly=" parameter to FormEngine
6613
     * @param array $conf TypoScript properties for configuring the edit icons.
6614
     * @param string $currentRecord The "table:uid" of the record being shown. If empty string then $this->currentRecord is used. For new records (set by $conf['newRecordFromTable']) it's auto-generated to "[tablename]:NEW
6615
     * @param array $dataArray Alternative data array to use. Default is $this->data
6616
     * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine
6617
     * @return string The input content string, possibly with edit icons added (not necessarily in the end but just after the last string of normal content.
6618
     * @deprecated since v11, will be removed with v12. Drop together with other editIcons removals.
6619
     */
6620
    public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '')
6621
    {
6622
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6623
            return $content;
6624
        }
6625
        if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) {
6626
            return $content;
6627
        }
6628
        if (!$currentRecord) {
6629
            $currentRecord = $this->currentRecord;
6630
        }
6631
        if (empty($dataArray)) {
6632
            $dataArray = $this->data;
6633
        }
6634
        // Check incoming params:
6635
        [$currentRecordTable, $currentRecordUID] = explode(':', $currentRecord);
6636
        [$fieldList, $table] = array_reverse(GeneralUtility::trimExplode(':', $params, true));
6637
        // Reverse the array because table is optional
6638
        if (!$table) {
6639
            $table = $currentRecordTable;
6640
        } elseif ($table != $currentRecordTable) {
6641
            // If the table is set as the first parameter, and does not match the table of the current record, then just return.
6642
            return $content;
6643
        }
6644
6645
        $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID;
6646
        // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
6647
        if (!array_key_exists('allow', $conf)) {
6648
            $conf['allow'] = 'edit';
6649
        }
6650
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6651
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6652
            if ($editClass) {
6653
                trigger_error('Hook "typo3/classes/class.frontendedit.php" is deprecated together with stdWrap.editIcons and will be removed in TYPO3 12.0.', E_USER_DEPRECATED);
6654
                $edit = GeneralUtility::makeInstance($editClass);
6655
                $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
6656
            }
6657
        }
6658
        return $content;
6659
    }
6660
6661
    /**
6662
     * Returns TRUE if the input table/row would be hidden in the frontend (according nto the current time and simulate user group)
6663
     *
6664
     * @param string $table The table name
6665
     * @param array $row The data record
6666
     * @return bool
6667
     * @internal
6668
     * @deprecated since v11, will be removed with v12. Unused.
6669
     */
6670
    public function isDisabled($table, $row)
6671
    {
6672
        trigger_error('Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 12.0.', E_USER_DEPRECATED);
6673
        $tsfe = $this->getTypoScriptFrontendController();
6674
        $enablecolumns = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
6675
        return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']]
6676
            || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup
6677
            || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > $GLOBALS['EXEC_TIME']
6678
            || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < $GLOBALS['EXEC_TIME'];
6679
    }
6680
6681
    /**
6682
     * Get instance of FAL resource factory
6683
     *
6684
     * @return ResourceFactory
6685
     */
6686
    protected function getResourceFactory()
6687
    {
6688
        return GeneralUtility::makeInstance(ResourceFactory::class);
6689
    }
6690
6691
    /**
6692
     * Wrapper function for GeneralUtility::getIndpEnv()
6693
     *
6694
     * @see GeneralUtility::getIndpEnv
6695
     * @param string $key Name of the "environment variable"/"server variable" you wish to get.
6696
     * @return string
6697
     */
6698
    protected function getEnvironmentVariable($key)
6699
    {
6700
        if ($key === 'REQUEST_URI') {
6701
            return $this->getRequest()->getAttribute('normalizedParams')->getRequestUri();
6702
        }
6703
        return GeneralUtility::getIndpEnv($key);
6704
    }
6705
6706
    /**
6707
     * Fetches content from cache
6708
     *
6709
     * @param array $configuration Array
6710
     * @return string|bool FALSE on cache miss
6711
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
6712
     */
6713
    protected function getFromCache(array $configuration)
6714
    {
6715
        $content = false;
6716
6717
        if ($this->getTypoScriptFrontendController()->no_cache) {
6718
            return $content;
6719
        }
6720
        $cacheKey = $this->calculateCacheKey($configuration);
6721
        if (!empty($cacheKey)) {
6722
            /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
6723
            $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)
6724
                ->getCache('hash');
6725
            $content = $cacheFrontend->get($cacheKey);
6726
        }
6727
        return $content;
6728
    }
6729
6730
    /**
6731
     * Calculates the lifetime of a cache entry based on the given configuration
6732
     *
6733
     * @param array $configuration
6734
     * @return int|null
6735
     */
6736
    protected function calculateCacheLifetime(array $configuration)
6737
    {
6738
        $configuration['lifetime'] = $configuration['lifetime'] ?? '';
6739
        $lifetimeConfiguration = (string)$this->stdWrapValue('lifetime', $configuration);
6740
6741
        $lifetime = null; // default lifetime
6742
        if (strtolower($lifetimeConfiguration) === 'unlimited') {
6743
            $lifetime = 0; // unlimited
6744
        } elseif ($lifetimeConfiguration > 0) {
6745
            $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds
6746
        }
6747
        return $lifetime;
6748
    }
6749
6750
    /**
6751
     * Calculates the tags for a cache entry bases on the given configuration
6752
     *
6753
     * @param array $configuration
6754
     * @return array
6755
     */
6756
    protected function calculateCacheTags(array $configuration)
6757
    {
6758
        $configuration['tags'] = $configuration['tags'] ?? '';
6759
        $tags = (string)$this->stdWrapValue('tags', $configuration);
6760
        return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags);
6761
    }
6762
6763
    /**
6764
     * Applies stdWrap to the cache key
6765
     *
6766
     * @param array $configuration
6767
     * @return string
6768
     */
6769
    protected function calculateCacheKey(array $configuration)
6770
    {
6771
        $configuration['key'] = $configuration['key'] ?? '';
6772
        return $this->stdWrapValue('key', $configuration);
6773
    }
6774
6775
    /**
6776
     * Returns the current BE user.
6777
     *
6778
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
6779
     */
6780
    protected function getFrontendBackendUser()
6781
    {
6782
        return $GLOBALS['BE_USER'];
6783
    }
6784
6785
    /**
6786
     * @return TimeTracker
6787
     */
6788
    protected function getTimeTracker()
6789
    {
6790
        return GeneralUtility::makeInstance(TimeTracker::class);
6791
    }
6792
6793
    /**
6794
     * @return TypoScriptFrontendController|null
6795
     * @internal this is set to public so extensions such as EXT:solr can use the method in tests.
6796
     */
6797
    public function getTypoScriptFrontendController()
6798
    {
6799
        return $this->typoScriptFrontendController ?: $GLOBALS['TSFE'] ?? null;
6800
    }
6801
6802
    /**
6803
     * Support anchors without href value
6804
     * Changes ContentObjectRenderer::typolink to render a tag without href,
6805
     * if id or name attribute is present.
6806
     *
6807
     * @param string $linkText
6808
     * @param array $conf Typolink configuration decoded as array
6809
     * @return string Full a-Tag or just the linktext if id or name are not set.
6810
     */
6811
    protected function resolveAnchorLink(string $linkText, array $conf): string
6812
    {
6813
        $anchorTag = '<a ' . $this->getATagParams($conf) . '>';
6814
        $aTagParams = GeneralUtility::get_tag_attributes($anchorTag);
6815
        // If it looks like a anchor tag, render it anyway
6816
        if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
6817
            return $anchorTag . $linkText . '</a>';
6818
        }
6819
        // Otherwise just return the link text
6820
        return $linkText;
6821
    }
6822
6823
    /**
6824
     * Get content length of the current tag that could also contain nested tag contents
6825
     *
6826
     * @param string $theValue
6827
     * @param int $pointer
6828
     * @param string $currentTag
6829
     * @return int
6830
     */
6831
    protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int
6832
    {
6833
        $tempContent = strtolower(substr($theValue, $pointer));
6834
        $startTag = '<' . $currentTag;
6835
        $endTag = '</' . $currentTag . '>';
6836
        $offsetCount = 0;
6837
6838
        // Take care for nested tags
6839
        do {
6840
            $nextMatchingEndTagPosition = strpos($tempContent, $endTag);
6841
            // only match tag `a` in `<a href"...">` but not in `<abbr>`
6842
            $nextSameTypeTagPosition = preg_match(
6843
                '#' . $startTag . '[\s/>]#',
6844
                $tempContent,
6845
                $nextSameStartTagMatches,
6846
                PREG_OFFSET_CAPTURE
6847
            ) ? $nextSameStartTagMatches[0][1] : false;
6848
6849
            // filter out nested tag contents to help getting the correct closing tag
6850
            if ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) {
6851
                $lastOpeningTagStartPosition = (int)strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag);
6852
                $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag);
6853
                $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition;
6854
6855
                // replace content from latest tag start to latest tag end
6856
                $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition);
6857
            }
6858
        } while (
6859
            ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) &&
6860
            $nextSameTypeTagPosition < $nextMatchingEndTagPosition
6861
        );
6862
6863
        // if no closing tag is found we use length of the whole content
6864
        $endingOffset = strlen($tempContent);
6865
        if ($nextMatchingEndTagPosition !== false) {
6866
            $endingOffset = $nextMatchingEndTagPosition + $offsetCount;
6867
        }
6868
6869
        return $endingOffset;
6870
    }
6871
6872
    protected function shallDebug(): bool
6873
    {
6874
        $tsfe = $this->getTypoScriptFrontendController();
6875
        if ($tsfe !== null && isset($tsfe->config['config']['debug'])) {
6876
            return (bool)($tsfe->config['config']['debug']);
6877
        }
6878
        return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
6879
    }
6880
6881
    public function getRequest(): ServerRequestInterface
6882
    {
6883
        if ($this->request instanceof ServerRequestInterface) {
6884
            return $this->request;
6885
        }
6886
6887
        if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
6888
            return $GLOBALS['TYPO3_REQUEST'];
6889
        }
6890
6891
        throw new ContentRenderingException('PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972);
6892
    }
6893
}
6894