Completed
Push — master ( 785ff6...8cec79 )
by
unknown
28:03 queued 12:03
created

ContentObjectRenderer::setRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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\DBALException;
19
use Doctrine\DBAL\Driver\Statement;
20
use Psr\Container\ContainerInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Log\LoggerAwareInterface;
23
use Psr\Log\LoggerAwareTrait;
24
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
25
use TYPO3\CMS\Core\Cache\CacheManager;
26
use TYPO3\CMS\Core\Context\Context;
27
use TYPO3\CMS\Core\Context\LanguageAspect;
28
use TYPO3\CMS\Core\Core\Environment;
29
use TYPO3\CMS\Core\Database\ConnectionPool;
30
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
31
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32
use TYPO3\CMS\Core\Database\Query\QueryHelper;
33
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
34
use TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction;
35
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
36
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
37
use TYPO3\CMS\Core\Html\HtmlParser;
38
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
39
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
40
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
41
use TYPO3\CMS\Core\LinkHandling\LinkService;
42
use TYPO3\CMS\Core\Log\LogManager;
43
use TYPO3\CMS\Core\Page\AssetCollector;
44
use TYPO3\CMS\Core\Resource\Exception;
45
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
46
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
47
use TYPO3\CMS\Core\Resource\File;
48
use TYPO3\CMS\Core\Resource\FileInterface;
49
use TYPO3\CMS\Core\Resource\FileReference;
50
use TYPO3\CMS\Core\Resource\ProcessedFile;
51
use TYPO3\CMS\Core\Resource\ResourceFactory;
52
use TYPO3\CMS\Core\Service\DependencyOrderingService;
53
use TYPO3\CMS\Core\Service\FlexFormService;
54
use TYPO3\CMS\Core\Site\SiteFinder;
55
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
56
use TYPO3\CMS\Core\Type\BitSet;
57
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
58
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
59
use TYPO3\CMS\Core\Utility\ArrayUtility;
60
use TYPO3\CMS\Core\Utility\DebugUtility;
61
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
62
use TYPO3\CMS\Core\Utility\GeneralUtility;
63
use TYPO3\CMS\Core\Utility\HttpUtility;
64
use TYPO3\CMS\Core\Utility\MathUtility;
65
use TYPO3\CMS\Core\Utility\StringUtility;
66
use TYPO3\CMS\Core\Versioning\VersionState;
67
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
68
use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
69
use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
70
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
71
use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
72
use TYPO3\CMS\Frontend\Imaging\GifBuilder;
73
use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
74
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
75
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
76
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
77
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
78
79
/**
80
 * This class contains all main TypoScript features.
81
 * This includes the rendering of TypoScript content objects (cObjects).
82
 * Is the backbone of TypoScript Template rendering.
83
 *
84
 * There are lots of functions you can use from your include-scripts.
85
 * The class is normally instantiated and referred to as "cObj".
86
 * 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.
87
 */
88
class ContentObjectRenderer implements LoggerAwareInterface
89
{
90
    use LoggerAwareTrait;
91
92
    /**
93
     * @var ContainerInterface
94
     */
95
    protected $container;
96
97
    /**
98
     * @var array
99
     */
100
    public $align = [
101
        'center',
102
        'right',
103
        'left'
104
    ];
105
106
    /**
107
     * stdWrap functions in their correct order
108
     *
109
     * @see stdWrap()
110
     * @var string[]
111
     */
112
    public $stdWrapOrder = [
113
        'stdWrapPreProcess' => 'hook',
114
        // this is a placeholder for the first Hook
115
        'cacheRead' => 'hook',
116
        // this is a placeholder for checking if the content is available in cache
117
        'setContentToCurrent' => 'boolean',
118
        'setContentToCurrent.' => 'array',
119
        'addPageCacheTags' => 'string',
120
        'addPageCacheTags.' => 'array',
121
        'setCurrent' => 'string',
122
        'setCurrent.' => 'array',
123
        'lang.' => 'array',
124
        'data' => 'getText',
125
        'data.' => 'array',
126
        'field' => 'fieldName',
127
        'field.' => 'array',
128
        'current' => 'boolean',
129
        'current.' => 'array',
130
        'cObject' => 'cObject',
131
        'cObject.' => 'array',
132
        'numRows.' => 'array',
133
        'preUserFunc' => 'functionName',
134
        'stdWrapOverride' => 'hook',
135
        // this is a placeholder for the second Hook
136
        'override' => 'string',
137
        'override.' => 'array',
138
        'preIfEmptyListNum' => 'listNum',
139
        'preIfEmptyListNum.' => 'array',
140
        'ifNull' => 'string',
141
        'ifNull.' => 'array',
142
        'ifEmpty' => 'string',
143
        'ifEmpty.' => 'array',
144
        'ifBlank' => 'string',
145
        'ifBlank.' => 'array',
146
        'listNum' => 'listNum',
147
        'listNum.' => 'array',
148
        'trim' => 'boolean',
149
        'trim.' => 'array',
150
        'strPad.' => 'array',
151
        'stdWrap' => 'stdWrap',
152
        'stdWrap.' => 'array',
153
        'stdWrapProcess' => 'hook',
154
        // this is a placeholder for the third Hook
155
        'required' => 'boolean',
156
        'required.' => 'array',
157
        'if.' => 'array',
158
        'fieldRequired' => 'fieldName',
159
        'fieldRequired.' => 'array',
160
        'csConv' => 'string',
161
        'csConv.' => 'array',
162
        'parseFunc' => 'objectpath',
163
        'parseFunc.' => 'array',
164
        'HTMLparser' => 'boolean',
165
        'HTMLparser.' => 'array',
166
        'split.' => 'array',
167
        'replacement.' => 'array',
168
        'prioriCalc' => 'boolean',
169
        'prioriCalc.' => 'array',
170
        'char' => 'integer',
171
        'char.' => 'array',
172
        'intval' => 'boolean',
173
        'intval.' => 'array',
174
        'hash' => 'string',
175
        'hash.' => 'array',
176
        'round' => 'boolean',
177
        'round.' => 'array',
178
        'numberFormat.' => 'array',
179
        'expandList' => 'boolean',
180
        'expandList.' => 'array',
181
        'date' => 'dateconf',
182
        'date.' => 'array',
183
        'strtotime' => 'strtotimeconf',
184
        'strtotime.' => 'array',
185
        'strftime' => 'strftimeconf',
186
        'strftime.' => 'array',
187
        'age' => 'boolean',
188
        'age.' => 'array',
189
        'case' => 'case',
190
        'case.' => 'array',
191
        'bytes' => 'boolean',
192
        'bytes.' => 'array',
193
        'substring' => 'parameters',
194
        'substring.' => 'array',
195
        'cropHTML' => 'crop',
196
        'cropHTML.' => 'array',
197
        'stripHtml' => 'boolean',
198
        'stripHtml.' => 'array',
199
        'crop' => 'crop',
200
        'crop.' => 'array',
201
        'rawUrlEncode' => 'boolean',
202
        'rawUrlEncode.' => 'array',
203
        'htmlSpecialChars' => 'boolean',
204
        'htmlSpecialChars.' => 'array',
205
        'encodeForJavaScriptValue' => 'boolean',
206
        'encodeForJavaScriptValue.' => 'array',
207
        'doubleBrTag' => 'string',
208
        'doubleBrTag.' => 'array',
209
        'br' => 'boolean',
210
        'br.' => 'array',
211
        'brTag' => 'string',
212
        'brTag.' => 'array',
213
        'encapsLines.' => 'array',
214
        'keywords' => 'boolean',
215
        'keywords.' => 'array',
216
        'innerWrap' => 'wrap',
217
        'innerWrap.' => 'array',
218
        'innerWrap2' => 'wrap',
219
        'innerWrap2.' => 'array',
220
        'preCObject' => 'cObject',
221
        'preCObject.' => 'array',
222
        'postCObject' => 'cObject',
223
        'postCObject.' => 'array',
224
        'wrapAlign' => 'align',
225
        'wrapAlign.' => 'array',
226
        'typolink.' => 'array',
227
        'wrap' => 'wrap',
228
        'wrap.' => 'array',
229
        'noTrimWrap' => 'wrap',
230
        'noTrimWrap.' => 'array',
231
        'wrap2' => 'wrap',
232
        'wrap2.' => 'array',
233
        'dataWrap' => 'dataWrap',
234
        'dataWrap.' => 'array',
235
        'prepend' => 'cObject',
236
        'prepend.' => 'array',
237
        'append' => 'cObject',
238
        'append.' => 'array',
239
        'wrap3' => 'wrap',
240
        'wrap3.' => 'array',
241
        'orderedStdWrap' => 'stdWrap',
242
        'orderedStdWrap.' => 'array',
243
        'outerWrap' => 'wrap',
244
        'outerWrap.' => 'array',
245
        'insertData' => 'boolean',
246
        'insertData.' => 'array',
247
        'postUserFunc' => 'functionName',
248
        'postUserFuncInt' => 'functionName',
249
        'prefixComment' => 'string',
250
        'prefixComment.' => 'array',
251
        'editIcons' => 'string',
252
        'editIcons.' => 'array',
253
        'editPanel' => 'boolean',
254
        'editPanel.' => 'array',
255
        'cacheStore' => 'hook',
256
        // this is a placeholder for storing the content in cache
257
        'stdWrapPostProcess' => 'hook',
258
        // this is a placeholder for the last Hook
259
        'debug' => 'boolean',
260
        'debug.' => 'array',
261
        'debugFunc' => 'boolean',
262
        'debugFunc.' => 'array',
263
        'debugData' => 'boolean',
264
        'debugData.' => 'array'
265
    ];
266
267
    /**
268
     * Class names for accordant content object names
269
     *
270
     * @var array
271
     */
272
    protected $contentObjectClassMap = [];
273
274
    /**
275
     * Loaded with the current data-record.
276
     *
277
     * If the instance of this class is used to render records from the database those records are found in this array.
278
     * The function stdWrap has TypoScript properties that fetch field-data from this array.
279
     *
280
     * @var array
281
     * @see start()
282
     */
283
    public $data = [];
284
285
    /**
286
     * @var string
287
     */
288
    protected $table = '';
289
290
    /**
291
     * Used for backup
292
     *
293
     * @var array
294
     */
295
    public $oldData = [];
296
297
    /**
298
     * If this is set with an array before stdWrap, it's used instead of $this->data in the data-property in stdWrap
299
     *
300
     * @var string
301
     */
302
    public $alternativeData = '';
303
304
    /**
305
     * Used by the parseFunc function and is loaded with tag-parameters when parsing tags.
306
     *
307
     * @var array
308
     */
309
    public $parameters = [];
310
311
    /**
312
     * @var string
313
     */
314
    public $currentValKey = 'currentValue_kidjls9dksoje';
315
316
    /**
317
     * This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation.
318
     * Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered.
319
     *
320
     * @var string
321
     */
322
    public $currentRecord = '';
323
324
    /**
325
     * Set in RecordsContentObject and ContentContentObject to the current number of records selected in a query.
326
     *
327
     * @var int
328
     */
329
    public $currentRecordTotal = 0;
330
331
    /**
332
     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
333
     *
334
     * @var int
335
     */
336
    public $currentRecordNumber = 0;
337
338
    /**
339
     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
340
     *
341
     * @var int
342
     */
343
    public $parentRecordNumber = 0;
344
345
    /**
346
     * 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.
347
     *
348
     * @var array
349
     */
350
    public $parentRecord = [];
351
352
    /**
353
     * @var string|int
354
     */
355
    public $checkPid_badDoktypeList = PageRepository::DOKTYPE_RECYCLER;
356
357
    /**
358
     * This will be set by typoLink() to the url of the most recent link created.
359
     *
360
     * @var string
361
     */
362
    public $lastTypoLinkUrl = '';
363
364
    /**
365
     * DO. link target.
366
     *
367
     * @var string
368
     */
369
    public $lastTypoLinkTarget = '';
370
371
    /**
372
     * @var array
373
     */
374
    public $lastTypoLinkLD = [];
375
376
    /**
377
     * array that registers rendered content elements (or any table) to make sure they are not rendered recursively!
378
     *
379
     * @var array
380
     */
381
    public $recordRegister = [];
382
383
    /**
384
     * Containing hook objects for stdWrap
385
     *
386
     * @var array
387
     */
388
    protected $stdWrapHookObjects = [];
389
390
    /**
391
     * Containing hook objects for getImgResource
392
     *
393
     * @var array
394
     */
395
    protected $getImgResourceHookObjects;
396
397
    /**
398
     * @var File|FileReference|Folder|null Current file objects (during iterations over files)
0 ignored issues
show
Bug introduced by
The type TYPO3\CMS\Frontend\ContentObject\Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
399
     */
400
    protected $currentFile;
401
402
    /**
403
     * Set to TRUE by doConvertToUserIntObject() if USER object wants to become USER_INT
404
     * @var bool
405
     */
406
    public $doConvertToUserIntObject = false;
407
408
    /**
409
     * Indicates current object type. Can hold one of OBJECTTYPE_ constants or FALSE.
410
     * The value is set and reset inside USER() function. Any time outside of
411
     * USER() it is FALSE.
412
     * @var bool
413
     */
414
    protected $userObjectType = false;
415
416
    /**
417
     * @var array
418
     */
419
    protected $stopRendering = [];
420
421
    /**
422
     * @var int
423
     */
424
    protected $stdWrapRecursionLevel = 0;
425
426
    /**
427
     * @var TypoScriptFrontendController|null
428
     */
429
    protected $typoScriptFrontendController;
430
431
    /**
432
     * Request pointer, if injected. Use getRequest() instead of reading this property directly.
433
     *
434
     * @var ServerRequestInterface|null
435
     */
436
    private ?ServerRequestInterface $request = null;
437
438
    /**
439
     * Indicates that object type is USER.
440
     *
441
     * @see ContentObjectRender::$userObjectType
442
     */
443
    const OBJECTTYPE_USER_INT = 1;
444
    /**
445
     * Indicates that object type is USER.
446
     *
447
     * @see ContentObjectRender::$userObjectType
448
     */
449
    const OBJECTTYPE_USER = 2;
450
451
    /**
452
     * @param TypoScriptFrontendController $typoScriptFrontendController
453
     * @param ContainerInterface $container
454
     */
455
    public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null, ContainerInterface $container = null)
456
    {
457
        $this->typoScriptFrontendController = $typoScriptFrontendController;
458
        $this->contentObjectClassMap = $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'];
459
        $this->container = $container;
460
    }
461
462
    public function setRequest(ServerRequestInterface $request): void
463
    {
464
        $this->request = $request;
465
    }
466
467
    /**
468
     * Prevent several objects from being serialized.
469
     * If currentFile is set, it is either a File or a FileReference object. As the object itself can't be serialized,
470
     * we have store a hash and restore the object in __wakeup()
471
     *
472
     * @return array
473
     */
474
    public function __sleep()
475
    {
476
        $vars = get_object_vars($this);
477
        unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container'], $vars['request']);
478
        if ($this->currentFile instanceof FileReference) {
479
            $this->currentFile = 'FileReference:' . $this->currentFile->getUid();
480
        } elseif ($this->currentFile instanceof File) {
481
            $this->currentFile = 'File:' . $this->currentFile->getIdentifier();
482
        } else {
483
            unset($vars['currentFile']);
484
        }
485
        return array_keys($vars);
486
    }
487
488
    /**
489
     * Restore currentFile from hash.
490
     * If currentFile references a File, the identifier equals file identifier.
491
     * If it references a FileReference the identifier equals the uid of the reference.
492
     */
493
    public function __wakeup()
494
    {
495
        if (isset($GLOBALS['TSFE'])) {
496
            $this->typoScriptFrontendController = $GLOBALS['TSFE'];
497
        }
498
        if ($this->currentFile !== null && is_string($this->currentFile)) {
0 ignored issues
show
introduced by
The condition is_string($this->currentFile) is always false.
Loading history...
499
            [$objectType, $identifier] = explode(':', $this->currentFile, 2);
500
            try {
501
                if ($objectType === 'File') {
502
                    $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($identifier);
503
                } elseif ($objectType === 'FileReference') {
504
                    $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject($identifier);
505
                }
506
            } catch (ResourceDoesNotExistException $e) {
507
                $this->currentFile = null;
508
            }
509
        }
510
        $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
511
        $this->container = GeneralUtility::getContainer();
512
513
        // We do not derive $this->request from globals here. The request is expected to be injected
514
        // using setRequest() after deserialization or with start().
515
        // (A fallback to $GLOBALS['TYPO3_REQUEST'] is available in getRequest() for BC)
516
    }
517
518
    /**
519
     * Allow injecting content object class map.
520
     *
521
     * This method is private API, please use configuration
522
     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
523
     *
524
     * @internal
525
     * @param array $contentObjectClassMap
526
     */
527
    public function setContentObjectClassMap(array $contentObjectClassMap)
528
    {
529
        $this->contentObjectClassMap = $contentObjectClassMap;
530
    }
531
532
    /**
533
     * Register a single content object name to class name
534
     *
535
     * This method is private API, please use configuration
536
     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
537
     *
538
     * @param string $className
539
     * @param string $contentObjectName
540
     * @internal
541
     */
542
    public function registerContentObjectClass($className, $contentObjectName)
543
    {
544
        $this->contentObjectClassMap[$contentObjectName] = $className;
545
    }
546
547
    /**
548
     * Class constructor.
549
     * Well, it has to be called manually since it is not a real constructor function.
550
     * 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.
551
     *
552
     * @param array $data The record data that is rendered.
553
     * @param string $table The table that the data record is from.
554
     * @param ServerRequestInterface|null $request
555
     */
556
    public function start($data, $table = '', ?ServerRequestInterface $request = null)
557
    {
558
        $this->request = $request ?? $this->request;
559
        $this->data = $data;
560
        $this->table = $table;
561
        $this->currentRecord = $table !== ''
562
            ? $table . ':' . ($this->data['uid'] ?? '')
563
            : '';
564
        $this->parameters = [];
565
        $this->stdWrapHookObjects = [];
566
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] ?? [] as $className) {
567
            $hookObject = GeneralUtility::makeInstance($className);
568
            if (!$hookObject instanceof ContentObjectStdWrapHookInterface) {
569
                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectStdWrapHookInterface::class, 1195043965);
570
            }
571
            $this->stdWrapHookObjects[] = $hookObject;
572
        }
573
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] ?? [] as $className) {
574
            $postInitializationProcessor = GeneralUtility::makeInstance($className);
575
            if (!$postInitializationProcessor instanceof ContentObjectPostInitHookInterface) {
576
                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectPostInitHookInterface::class, 1274563549);
577
            }
578
            $postInitializationProcessor->postProcessContentObjectInitialization($this);
579
        }
580
    }
581
582
    /**
583
     * Returns the current table
584
     *
585
     * @return string
586
     */
587
    public function getCurrentTable()
588
    {
589
        return $this->table;
590
    }
591
592
    /**
593
     * Gets the 'getImgResource' hook objects.
594
     * The first call initializes the accordant objects.
595
     *
596
     * @return array The 'getImgResource' hook objects (if any)
597
     */
598
    protected function getGetImgResourceHookObjects()
599
    {
600
        if (!isset($this->getImgResourceHookObjects)) {
601
            $this->getImgResourceHookObjects = [];
602
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] ?? [] as $className) {
603
                $hookObject = GeneralUtility::makeInstance($className);
604
                if (!$hookObject instanceof ContentObjectGetImageResourceHookInterface) {
605
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetImageResourceHookInterface::class, 1218636383);
606
                }
607
                $this->getImgResourceHookObjects[] = $hookObject;
608
            }
609
        }
610
        return $this->getImgResourceHookObjects;
611
    }
612
613
    /**
614
     * Sets the internal variable parentRecord with information about current record.
615
     * 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.
616
     *
617
     * @param array $data The record array
618
     * @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.
619
     * @internal
620
     */
621
    public function setParent($data, $currentRecord)
622
    {
623
        $this->parentRecord = [
624
            'data' => $data,
625
            'currentRecord' => $currentRecord
626
        ];
627
    }
628
629
    /***********************************************
630
     *
631
     * CONTENT_OBJ:
632
     *
633
     ***********************************************/
634
    /**
635
     * Returns the "current" value.
636
     * 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.
637
     * It's like "load accumulator" in the good old C64 days... basically a "register" you can use as you like.
638
     * 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.
639
     *
640
     * @return mixed The "current" value
641
     */
642
    public function getCurrentVal()
643
    {
644
        return $this->data[$this->currentValKey];
645
    }
646
647
    /**
648
     * Sets the "current" value.
649
     *
650
     * @param mixed $value The variable that you want to set as "current
651
     * @see getCurrentVal()
652
     */
653
    public function setCurrentVal($value)
654
    {
655
        $this->data[$this->currentValKey] = $value;
656
    }
657
658
    /**
659
     * Rendering of a "numerical array" of cObjects from TypoScript
660
     * Will call ->cObjGetSingle() for each cObject found and accumulate the output.
661
     *
662
     * @param array $setup array with cObjects as values.
663
     * @param string $addKey A prefix for the debugging information
664
     * @return string Rendered output from the cObjects in the array.
665
     * @see cObjGetSingle()
666
     */
667
    public function cObjGet($setup, $addKey = '')
668
    {
669
        if (!is_array($setup)) {
0 ignored issues
show
introduced by
The condition is_array($setup) is always true.
Loading history...
670
            return '';
671
        }
672
        $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($setup);
673
        $content = '';
674
        foreach ($sKeyArray as $theKey) {
675
            $theValue = $setup[$theKey];
676
            if ((int)$theKey && strpos($theKey, '.') === false) {
677
                $conf = $setup[$theKey . '.'];
678
                $content .= $this->cObjGetSingle($theValue, $conf, $addKey . $theKey);
679
            }
680
        }
681
        return $content;
682
    }
683
684
    /**
685
     * Renders a content object
686
     *
687
     * @param string $name The content object name, eg. "TEXT" or "USER" or "IMAGE"
688
     * @param array $conf The array with TypoScript properties for the content object
689
     * @param string $TSkey A string label used for the internal debugging tracking.
690
     * @return string cObject output
691
     * @throws \UnexpectedValueException
692
     */
693
    public function cObjGetSingle($name, $conf, $TSkey = '__')
694
    {
695
        $content = '';
696
        // Checking that the function is not called eternally. This is done by interrupting at a depth of 100
697
        $this->getTypoScriptFrontendController()->cObjectDepthCounter--;
698
        if ($this->getTypoScriptFrontendController()->cObjectDepthCounter > 0) {
699
            $timeTracker = $this->getTimeTracker();
700
            $name = trim($name);
701
            if ($timeTracker->LR) {
702
                $timeTracker->push($TSkey, $name);
703
            }
704
            // Checking if the COBJ is a reference to another object. (eg. name of 'some.object =< styles.something')
705
            if (isset($name[0]) && $name[0] === '<') {
706
                $key = trim(substr($name, 1));
707
                $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
708
                // $name and $conf is loaded with the referenced values.
709
                $confOverride = is_array($conf) ? $conf : [];
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
710
                [$name, $conf] = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
711
                $conf = array_replace_recursive(is_array($conf) ? $conf : [], $confOverride);
712
                // Getting the cObject
713
                $timeTracker->incStackPointer();
714
                $content .= $this->cObjGetSingle($name, $conf, $key);
715
                $timeTracker->decStackPointer();
716
            } else {
717
                $contentObject = $this->getContentObject($name);
718
                if ($contentObject) {
719
                    $content .= $this->render($contentObject, $conf);
720
                }
721
            }
722
            if ($timeTracker->LR) {
723
                $timeTracker->pull($content);
724
            }
725
        }
726
        // Increasing on exit...
727
        $this->getTypoScriptFrontendController()->cObjectDepthCounter++;
728
        return $content;
729
    }
730
731
    /**
732
     * Returns a new content object of type $name.
733
     * This content object needs to be registered as content object
734
     * in $this->contentObjectClassMap
735
     *
736
     * @param string $name
737
     * @return AbstractContentObject|null
738
     * @throws ContentRenderingException
739
     */
740
    public function getContentObject($name)
741
    {
742
        if (!isset($this->contentObjectClassMap[$name])) {
743
            return null;
744
        }
745
        $fullyQualifiedClassName = $this->contentObjectClassMap[$name];
746
        $contentObject = GeneralUtility::makeInstance($fullyQualifiedClassName, $this);
747
        if (!($contentObject instanceof AbstractContentObject)) {
748
            throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295);
749
        }
750
        $contentObject->setRequest($this->getRequest());
751
        return $contentObject;
752
    }
753
754
    /********************************************
755
     *
756
     * Functions rendering content objects (cObjects)
757
     *
758
     ********************************************/
759
    /**
760
     * Renders a content object by taking exception and cache handling
761
     * into consideration
762
     *
763
     * @param AbstractContentObject $contentObject Content object instance
764
     * @param array $configuration Array of TypoScript properties
765
     *
766
     * @throws ContentRenderingException
767
     * @throws \Exception
768
     * @return string
769
     */
770
    public function render(AbstractContentObject $contentObject, $configuration = [])
771
    {
772
        $content = '';
773
774
        // Evaluate possible cache and return
775
        $cacheConfiguration = $configuration['cache.'] ?? null;
776
        if ($cacheConfiguration !== null) {
777
            unset($configuration['cache.']);
778
            $cache = $this->getFromCache($cacheConfiguration);
779
            if ($cache !== false) {
780
                return $cache;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $cache also could return the type true which is incompatible with the documented return type string.
Loading history...
781
            }
782
        }
783
784
        // Render content
785
        try {
786
            $content .= $contentObject->render($configuration);
787
        } catch (ContentRenderingException $exception) {
788
            // Content rendering Exceptions indicate a critical problem which should not be
789
            // caught e.g. when something went wrong with Exception handling itself
790
            throw $exception;
791
        } catch (\Exception $exception) {
792
            $exceptionHandler = $this->createExceptionHandler($configuration);
793
            if ($exceptionHandler === null) {
794
                throw $exception;
795
            }
796
            $content = $exceptionHandler->handle($exception, $contentObject, $configuration);
797
        }
798
799
        // Store cache
800
        if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) {
801
            $key = $this->calculateCacheKey($cacheConfiguration);
802
            if (!empty($key)) {
803
                /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
804
                $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
805
                $tags = $this->calculateCacheTags($cacheConfiguration);
806
                $lifetime = $this->calculateCacheLifetime($cacheConfiguration);
807
                $cacheFrontend->set($key, $content, $tags, $lifetime);
808
            }
809
        }
810
811
        return $content;
812
    }
813
814
    /**
815
     * Creates the content object exception handler from local content object configuration
816
     * or, from global configuration if not explicitly disabled in local configuration
817
     *
818
     * @param array $configuration
819
     * @return ExceptionHandlerInterface|null
820
     * @throws ContentRenderingException
821
     */
822
    protected function createExceptionHandler($configuration = [])
823
    {
824
        $exceptionHandler = null;
825
        $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration);
826
        if (!empty($exceptionHandlerClassName)) {
827
            $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName, $this->mergeExceptionHandlerConfiguration($configuration));
828
            if (!$exceptionHandler instanceof ExceptionHandlerInterface) {
829
                throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369);
830
            }
831
        }
832
833
        return $exceptionHandler;
834
    }
835
836
    /**
837
     * Determine exception handler class name from global and content object configuration
838
     *
839
     * @param array $configuration
840
     * @return string|null
841
     */
842
    protected function determineExceptionHandlerClassName($configuration)
843
    {
844
        $exceptionHandlerClassName = null;
845
        $tsfe = $this->getTypoScriptFrontendController();
846
        if (!isset($tsfe->config['config']['contentObjectExceptionHandler'])) {
847
            if (Environment::getContext()->isProduction()) {
848
                $exceptionHandlerClassName = '1';
849
            }
850
        } else {
851
            $exceptionHandlerClassName = $tsfe->config['config']['contentObjectExceptionHandler'];
852
        }
853
854
        if (isset($configuration['exceptionHandler'])) {
855
            $exceptionHandlerClassName = $configuration['exceptionHandler'];
856
        }
857
858
        if ($exceptionHandlerClassName === '1') {
859
            $exceptionHandlerClassName = ProductionExceptionHandler::class;
860
        }
861
862
        return $exceptionHandlerClassName;
863
    }
864
865
    /**
866
     * Merges global exception handler configuration with the one from the content object
867
     * and returns the merged exception handler configuration
868
     *
869
     * @param array $configuration
870
     * @return array
871
     */
872
    protected function mergeExceptionHandlerConfiguration($configuration)
873
    {
874
        $exceptionHandlerConfiguration = [];
875
        $tsfe = $this->getTypoScriptFrontendController();
876
        if (!empty($tsfe->config['config']['contentObjectExceptionHandler.'])) {
877
            $exceptionHandlerConfiguration = $tsfe->config['config']['contentObjectExceptionHandler.'];
878
        }
879
        if (!empty($configuration['exceptionHandler.'])) {
880
            $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']);
881
        }
882
883
        return $exceptionHandlerConfiguration;
884
    }
885
886
    /**
887
     * Retrieves a type of object called as USER or USER_INT. Object can detect their
888
     * type by using this call. It returns OBJECTTYPE_USER_INT or OBJECTTYPE_USER depending on the
889
     * current object execution. In all other cases it will return FALSE to indicate
890
     * a call out of context.
891
     *
892
     * @return mixed One of OBJECTTYPE_ class constants or FALSE
893
     */
894
    public function getUserObjectType()
895
    {
896
        return $this->userObjectType;
897
    }
898
899
    /**
900
     * Sets the user object type
901
     *
902
     * @param mixed $userObjectType
903
     */
904
    public function setUserObjectType($userObjectType)
905
    {
906
        $this->userObjectType = $userObjectType;
907
    }
908
909
    /**
910
     * Requests the current USER object to be converted to USER_INT.
911
     */
912
    public function convertToUserIntObject()
913
    {
914
        if ($this->userObjectType !== self::OBJECTTYPE_USER) {
0 ignored issues
show
introduced by
The condition $this->userObjectType !== self::OBJECTTYPE_USER is always true.
Loading history...
915
            $this->getTimeTracker()->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', 2);
916
        } else {
917
            $this->doConvertToUserIntObject = true;
918
        }
919
    }
920
921
    /************************************
922
     *
923
     * Various helper functions for content objects:
924
     *
925
     ************************************/
926
    /**
927
     * Converts a given config in Flexform to a conf-array
928
     *
929
     * @param string|array $flexData Flexform data
930
     * @param array $conf Array to write the data into, by reference
931
     * @param bool $recursive Is set if called recursive. Don't call function with this parameter, it's used inside the function only
932
     */
933
    public function readFlexformIntoConf($flexData, &$conf, $recursive = false)
934
    {
935
        if ($recursive === false && is_string($flexData)) {
936
            $flexData = GeneralUtility::xml2array($flexData, 'T3');
937
        }
938
        if (is_array($flexData) && isset($flexData['data']['sDEF']['lDEF'])) {
939
            $flexData = $flexData['data']['sDEF']['lDEF'];
940
        }
941
        if (!is_array($flexData)) {
942
            return;
943
        }
944
        foreach ($flexData as $key => $value) {
945
            if (!is_array($value)) {
946
                continue;
947
            }
948
            if (isset($value['el'])) {
949
                if (is_array($value['el']) && !empty($value['el'])) {
950
                    foreach ($value['el'] as $ekey => $element) {
951
                        if (isset($element['vDEF'])) {
952
                            $conf[$ekey] = $element['vDEF'];
953
                        } else {
954
                            if (is_array($element)) {
955
                                $this->readFlexformIntoConf($element, $conf[$key][key($element)][$ekey], true);
956
                            } else {
957
                                $this->readFlexformIntoConf($element, $conf[$key][$ekey], true);
958
                            }
959
                        }
960
                    }
961
                } else {
962
                    $this->readFlexformIntoConf($value['el'], $conf[$key], true);
963
                }
964
            }
965
            if (isset($value['vDEF'])) {
966
                $conf[$key] = $value['vDEF'];
967
            }
968
        }
969
    }
970
971
    /**
972
     * Returns all parents of the given PID (Page UID) list
973
     *
974
     * @param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap
975
     * @param array $pidConf stdWrap array for the list
976
     * @return string A list of PIDs
977
     * @internal
978
     */
979
    public function getSlidePids($pidList, $pidConf)
980
    {
981
        // todo: phpstan states that $pidConf always exists and is not nullable. At the moment, this is a false positive
982
        //       as null can be passed into this method via $pidConf. As soon as more strict types are used, this isset
983
        //       check must be replaced with a more appropriate check like empty or count.
984
        $pidList = isset($pidConf) ? trim($this->stdWrap($pidList, $pidConf)) : trim($pidList);
985
        if ($pidList === '') {
986
            $pidList = 'this';
987
        }
988
        $tsfe = $this->getTypoScriptFrontendController();
989
        $listArr = null;
990
        if (trim($pidList)) {
991
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $pidList));
992
            $listArr = $this->checkPidArray($listArr);
993
        }
994
        $pidList = [];
995
        if (is_array($listArr) && !empty($listArr)) {
996
            foreach ($listArr as $uid) {
997
                $page = $tsfe->sys_page->getPage($uid);
998
                if (!$page['is_siteroot']) {
999
                    $pidList[] = $page['pid'];
1000
                }
1001
            }
1002
        }
1003
        return implode(',', $pidList);
1004
    }
1005
1006
    /**
1007
     * Wraps the input string in link-tags that opens the image in a new window.
1008
     *
1009
     * @param string $string String to wrap, probably an <img> tag
1010
     * @param string|File|FileReference $imageFile The original image file
1011
     * @param array $conf TypoScript properties for the "imageLinkWrap" function
1012
     * @return string The input string, $string, wrapped as configured.
1013
     * @internal This method should be used within TYPO3 Core only
1014
     */
1015
    public function imageLinkWrap($string, $imageFile, $conf)
1016
    {
1017
        $string = (string)$string;
1018
        $enable = $this->stdWrapValue('enable', $conf ?? []);
1019
        if (!$enable) {
1020
            return $string;
1021
        }
1022
        $content = (string)$this->typoLink($string, $conf['typolink.']);
1023
        if (isset($conf['file.']) && is_scalar($imageFile)) {
1024
            $imageFile = $this->stdWrap((string)$imageFile, $conf['file.']);
1025
        }
1026
1027
        if ($imageFile instanceof File) {
1028
            $file = $imageFile;
1029
        } elseif ($imageFile instanceof FileReference) {
1030
            $file = $imageFile->getOriginalFile();
1031
        } else {
1032
            if (MathUtility::canBeInterpretedAsInteger($imageFile)) {
1033
                $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$imageFile);
1034
            } else {
1035
                $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObjectFromCombinedIdentifier($imageFile);
1036
            }
1037
        }
1038
1039
        // Create imageFileLink if not created with typolink
1040
        if ($content === $string && $file !== null) {
1041
            $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop'];
1042
            $parameters = [];
1043
            $sample = $this->stdWrapValue('sample', $conf ?? []);
1044
            if ($sample) {
1045
                $parameters['sample'] = 1;
1046
            }
1047
            foreach ($parameterNames as $parameterName) {
1048
                if (isset($conf[$parameterName . '.'])) {
1049
                    $conf[$parameterName] = $this->stdWrap($conf[$parameterName], $conf[$parameterName . '.']);
1050
                }
1051
                if (isset($conf[$parameterName]) && $conf[$parameterName]) {
1052
                    $parameters[$parameterName] = $conf[$parameterName];
1053
                }
1054
            }
1055
            $parametersEncoded = base64_encode((string)json_encode($parameters));
1056
            $hmac = GeneralUtility::hmac(implode('|', [$file->getUid(), $parametersEncoded]));
1057
            $params = '&md5=' . $hmac;
1058
            foreach (str_split($parametersEncoded, 64) as $index => $chunk) {
1059
                $params .= '&parameters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk);
1060
            }
1061
            $url = $this->getTypoScriptFrontendController()->absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params;
1062
            $directImageLink = $this->stdWrapValue('directImageLink', $conf ?? []);
1063
            if ($directImageLink) {
1064
                $imgResourceConf = [
1065
                    'file' => $imageFile,
1066
                    'file.' => $conf
1067
                ];
1068
                $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf);
1069
                if (!$url) {
1070
                    // If no imagemagick / gm is available
1071
                    $url = $imageFile;
1072
                }
1073
            }
1074
            // Create TARGET-attribute only if the right doctype is used
1075
            $target = '';
1076
            $xhtmlDocType = $this->getTypoScriptFrontendController()->xhtmlDoctype;
1077
            if ($xhtmlDocType !== 'xhtml_strict' && $xhtmlDocType !== 'xhtml_11') {
1078
                $target = (string)$this->stdWrapValue('target', $conf ?? []);
1079
                if ($target === '') {
1080
                    $target = 'thePicture';
1081
                }
1082
            }
1083
            $a1 = '';
1084
            $a2 = '';
1085
            $conf['JSwindow'] = $this->stdWrapValue('JSwindow', $conf ?? []);
1086
            if ($conf['JSwindow']) {
1087
                $altUrl = $this->stdWrapValue('altUrl', $conf['JSwindow.'] ?? []);
1088
                if ($altUrl) {
1089
                    $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode($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

1089
                    $url = /** @scrutinizer ignore-type */ $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode($imageFile) . $params);
Loading history...
Bug introduced by
It seems like $imageFile can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $str of rawurlencode() 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

1089
                    $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode(/** @scrutinizer ignore-type */ $imageFile) . $params);
Loading history...
1090
                }
1091
1092
                $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf);
1093
                $JSwindowExpand = $this->stdWrapValue('expand', $conf['JSwindow.'] ?? []);
1094
                $offset = GeneralUtility::intExplode(',', $JSwindowExpand . ',');
1095
                $newWindow = $this->stdWrapValue('newWindow', $conf['JSwindow.'] ?? []);
1096
                $params = [
1097
                    'width' => ($processedFile->getProperty('width') + $offset[0]),
1098
                    'height' => ($processedFile->getProperty('height') + $offset[1]),
1099
                    'status' => '0',
1100
                    'menubar' => '0'
1101
                ];
1102
                // params override existing parameters from above, or add more
1103
                $windowParams = (string)$this->stdWrapValue('params', $conf['JSwindow.'] ?? []);
1104
                $windowParams = explode(',', $windowParams);
1105
                foreach ($windowParams as $windowParam) {
1106
                    [$paramKey, $paramValue] = explode('=', $windowParam);
1107
                    if ($paramValue !== '') {
1108
                        $params[$paramKey] = $paramValue;
1109
                    } else {
1110
                        unset($params[$paramKey]);
1111
                    }
1112
                }
1113
                $paramString = '';
1114
                foreach ($params as $paramKey => $paramValue) {
1115
                    $paramString .= htmlspecialchars((string)$paramKey) . '=' . htmlspecialchars((string)$paramValue) . ',';
1116
                }
1117
1118
                $onClick = 'openPic('
1119
                    . GeneralUtility::quoteJSvalue($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

1119
                    . GeneralUtility::quoteJSvalue($this->getTypoScriptFrontendController()->baseUrlWrap(/** @scrutinizer ignore-type */ $url)) . ','
Loading history...
1120
                    . '\'' . ($newWindow ? md5($url) : 'thePicture') . '\','
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 $str of md5() 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

1120
                    . '\'' . ($newWindow ? md5(/** @scrutinizer ignore-type */ $url) : 'thePicture') . '\','
Loading history...
1121
                    . GeneralUtility::quoteJSvalue(rtrim($paramString, ',')) . '); return false;';
1122
                $a1 = '<a href="' . htmlspecialchars($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 $string of htmlspecialchars() 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

1122
                $a1 = '<a href="' . htmlspecialchars(/** @scrutinizer ignore-type */ $url) . '"'
Loading history...
1123
                    . ' onclick="' . htmlspecialchars($onClick) . '"'
1124
                    . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
1125
                    . $this->getTypoScriptFrontendController()->ATagParams . '>';
1126
                $a2 = '</a>';
1127
                GeneralUtility::makeInstance(AssetCollector::class)->addInlineJavaScript('openPic', 'function openPic(url, winName, winParams) { var theWindow = window.open(url, winName, winParams); if (theWindow) { theWindow.focus(); } }');
1128
            } else {
1129
                $conf['linkParams.']['directImageLink'] = (bool)$conf['directImageLink'];
1130
                $conf['linkParams.']['parameter'] = $url;
1131
                $string = $this->typoLink($string, $conf['linkParams.']);
1132
            }
1133
            if (isset($conf['stdWrap.'])) {
1134
                $string = $this->stdWrap($string, $conf['stdWrap.']);
1135
            }
1136
            $content = $a1 . $string . $a2;
1137
        }
1138
        return $content;
1139
    }
1140
1141
    /**
1142
     * Sets the SYS_LASTCHANGED timestamp if input timestamp is larger than current value.
1143
     * The SYS_LASTCHANGED timestamp can be used by various caching/indexing applications to determine if the page has new content.
1144
     * Therefore you should call this function with the last-changed timestamp of any element you display.
1145
     *
1146
     * @param int $tstamp Unix timestamp (number of seconds since 1970)
1147
     * @see TypoScriptFrontendController::setSysLastChanged()
1148
     */
1149
    public function lastChanged($tstamp)
1150
    {
1151
        $tstamp = (int)$tstamp;
1152
        $tsfe = $this->getTypoScriptFrontendController();
1153
        if ($tstamp > (int)$tsfe->register['SYS_LASTCHANGED']) {
1154
            $tsfe->register['SYS_LASTCHANGED'] = $tstamp;
1155
        }
1156
    }
1157
1158
    /**
1159
     * An abstraction method to add parameters to an A tag.
1160
     * Uses the ATagParams property.
1161
     *
1162
     * @param array $conf TypoScript configuration properties
1163
     * @param bool|int $addGlobal If set, will add the global config.ATagParams to the link
1164
     * @return string String containing the parameters to the A tag (if non empty, with a leading space)
1165
     * @see typolink()
1166
     */
1167
    public function getATagParams($conf, $addGlobal = 1)
1168
    {
1169
        $aTagParams = ' ' . $this->stdWrapValue('ATagParams', $conf ?? []);
1170
        if ($addGlobal) {
1171
            $aTagParams = ' ' . trim($this->getTypoScriptFrontendController()->ATagParams . $aTagParams);
1172
        }
1173
        // Extend params
1174
        $_params = [
1175
            'conf' => &$conf,
1176
            'aTagParams' => &$aTagParams
1177
        ];
1178
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ?? [] as $className) {
1179
            $processor = GeneralUtility::makeInstance($className);
1180
            $aTagParams = $processor->process($_params, $this);
1181
        }
1182
1183
        $aTagParams = trim($aTagParams);
1184
        if (!empty($aTagParams)) {
1185
            $aTagParams = ' ' . $aTagParams;
1186
        }
1187
1188
        return $aTagParams;
1189
    }
1190
1191
    /***********************************************
1192
     *
1193
     * HTML template processing functions
1194
     *
1195
     ***********************************************/
1196
1197
    /**
1198
     * Sets the current file object during iterations over files.
1199
     *
1200
     * @param File $fileObject The file object.
1201
     */
1202
    public function setCurrentFile($fileObject)
1203
    {
1204
        $this->currentFile = $fileObject;
1205
    }
1206
1207
    /**
1208
     * Gets the current file object during iterations over files.
1209
     *
1210
     * @return File The current file object.
1211
     */
1212
    public function getCurrentFile()
1213
    {
1214
        return $this->currentFile;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->currentFile also could return the type TYPO3\CMS\Core\Resource\FileReference which is incompatible with the documented return type TYPO3\CMS\Core\Resource\File.
Loading history...
1215
    }
1216
1217
    /***********************************************
1218
     *
1219
     * "stdWrap" + sub functions
1220
     *
1221
     ***********************************************/
1222
    /**
1223
     * The "stdWrap" function. This is the implementation of what is known as "stdWrap properties" in TypoScript.
1224
     * Basically "stdWrap" performs some processing of a value based on properties in the input $conf array(holding the TypoScript "stdWrap properties")
1225
     * 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.
1226
     *
1227
     * If $this->alternativeData is an array it's used instead of the $this->data array in ->getData
1228
     *
1229
     * @param string $content Input value undergoing processing in this function. Possibly substituted by other values fetched from another source.
1230
     * @param array $conf TypoScript "stdWrap properties".
1231
     * @return string The processed input value
1232
     */
1233
    public function stdWrap($content = '', $conf = [])
1234
    {
1235
        $content = (string)$content;
1236
        // If there is any hook object, activate all of the process and override functions.
1237
        // The hook interface ContentObjectStdWrapHookInterface takes care that all 4 methods exist.
1238
        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...
1239
            $conf['stdWrapPreProcess'] = 1;
1240
            $conf['stdWrapOverride'] = 1;
1241
            $conf['stdWrapProcess'] = 1;
1242
            $conf['stdWrapPostProcess'] = 1;
1243
        }
1244
1245
        if (!is_array($conf) || !$conf) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
1246
            return $content;
1247
        }
1248
1249
        // Cache handling
1250
        if (isset($conf['cache.']) && is_array($conf['cache.'])) {
1251
            $conf['cache.']['key'] = $this->stdWrapValue('key', $conf['cache.'] ?? []);
1252
            $conf['cache.']['tags'] = $this->stdWrapValue('tags', $conf['cache.'] ?? []);
1253
            $conf['cache.']['lifetime'] = $this->stdWrapValue('lifetime', $conf['cache.'] ?? []);
1254
            $conf['cacheRead'] = 1;
1255
            $conf['cacheStore'] = 1;
1256
        }
1257
        // The configuration is sorted and filtered by intersection with the defined stdWrapOrder.
1258
        $sortedConf = array_keys(array_intersect_key($this->stdWrapOrder, $conf));
1259
        // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions
1260
        $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap';
1261
        // Additional Array to check whether a function has already been executed
1262
        $isExecuted = [];
1263
        // Additional switch to make sure 'required', 'if' and 'fieldRequired'
1264
        // will still stop rendering immediately in case they return FALSE
1265
        $this->stdWrapRecursionLevel++;
1266
        $this->stopRendering[$this->stdWrapRecursionLevel] = false;
1267
        // execute each function in the predefined order
1268
        foreach ($sortedConf as $stdWrapName) {
1269
            // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped
1270
            if ((!isset($isExecuted[$stdWrapName]) || !$isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapRecursionLevel]) {
1271
                $functionName = rtrim($stdWrapName, '.');
1272
                $functionProperties = $functionName . '.';
1273
                $functionType = $this->stdWrapOrder[$functionName] ?? '';
1274
                // If there is any code on the next level, check if it contains "official" stdWrap functions
1275
                // if yes, execute them first - will make each function stdWrap aware
1276
                // so additional stdWrap calls within the functions can be removed, since the result will be the same
1277
                if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) {
1278
                    if (array_intersect_key($this->stdWrapOrder, $conf[$functionProperties])) {
1279
                        // Check if there's already content available before processing
1280
                        // any ifEmpty or ifBlank stdWrap properties
1281
                        if (($functionName === 'ifBlank' && $content !== '') ||
1282
                            ($functionName === 'ifEmpty' && trim($content) !== '')) {
1283
                            continue;
1284
                        }
1285
1286
                        $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties] ?? []);
1287
                    }
1288
                }
1289
                // Check if key is still containing something, since it might have been changed by next level stdWrap before
1290
                if ((isset($conf[$functionName]) || $conf[$functionProperties])
1291
                    && ($functionType !== 'boolean' || $conf[$functionName])
1292
                ) {
1293
                    // Get just that part of $conf that is needed for the particular function
1294
                    $singleConf = [
1295
                        $functionName => $conf[$functionName] ?? null,
1296
                        $functionProperties => $conf[$functionProperties] ?? null
1297
                    ];
1298
                    // Hand over the whole $conf array to the stdWrapHookObjects
1299
                    if ($functionType === 'hook') {
1300
                        $singleConf = $conf;
1301
                    }
1302
                    // Add both keys - with and without the dot - to the set of executed functions
1303
                    $isExecuted[$functionName] = true;
1304
                    $isExecuted[$functionProperties] = true;
1305
                    // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array
1306
                    $functionName = 'stdWrap_' . $functionName;
1307
                    $content = $this->{$functionName}($content, $singleConf);
1308
                } elseif ($functionType === 'boolean' && !$conf[$functionName]) {
1309
                    $isExecuted[$functionName] = true;
1310
                    $isExecuted[$functionProperties] = true;
1311
                }
1312
            }
1313
        }
1314
        unset($this->stopRendering[$this->stdWrapRecursionLevel]);
1315
        $this->stdWrapRecursionLevel--;
1316
1317
        return $content;
1318
    }
1319
1320
    /**
1321
     * Gets a configuration value by passing them through stdWrap first and taking a default value if stdWrap doesn't yield a result.
1322
     *
1323
     * @param string $key The config variable key (from TS array).
1324
     * @param array $config The TypoScript array.
1325
     * @param string|int|bool|null $defaultValue Optional default value.
1326
     * @return string|int|bool|null Value of the config variable
1327
     */
1328
    public function stdWrapValue($key, array $config, $defaultValue = '')
1329
    {
1330
        if (isset($config[$key])) {
1331
            if (!isset($config[$key . '.'])) {
1332
                return $config[$key];
1333
            }
1334
        } elseif (isset($config[$key . '.'])) {
1335
            $config[$key] = '';
1336
        } else {
1337
            return $defaultValue;
1338
        }
1339
        $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']);
1340
        // The string "0" should be returned.
1341
        return $stdWrapped !== '' ? $stdWrapped : $defaultValue;
1342
    }
1343
1344
    /**
1345
     * stdWrap pre process hook
1346
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1347
     * this hook will execute functions before any other stdWrap function can modify anything
1348
     *
1349
     * @param string $content Input value undergoing processing in these functions.
1350
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1351
     * @return string The processed input value
1352
     */
1353
    public function stdWrap_stdWrapPreProcess($content = '', $conf = [])
1354
    {
1355
        foreach ($this->stdWrapHookObjects as $hookObject) {
1356
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1357
            $content = $hookObject->stdWrapPreProcess($content, $conf, $this);
1358
        }
1359
        return $content;
1360
    }
1361
1362
    /**
1363
     * Check if content was cached before (depending on the given cache key)
1364
     *
1365
     * @param string $content Input value undergoing processing in these functions.
1366
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1367
     * @return string The processed input value
1368
     */
1369
    public function stdWrap_cacheRead($content = '', $conf = [])
1370
    {
1371
        if (!isset($conf['cache.'])) {
1372
            return $content;
1373
        }
1374
        $result = $this->getFromCache($conf['cache.']);
1375
        return $result === false ? $content : $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result === false ? $content : $result also could return the type true which is incompatible with the documented return type string.
Loading history...
1376
    }
1377
1378
    /**
1379
     * Add tags to page cache (comma-separated list)
1380
     *
1381
     * @param string $content Input value undergoing processing in these functions.
1382
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1383
     * @return string The processed input value
1384
     */
1385
    public function stdWrap_addPageCacheTags($content = '', $conf = [])
1386
    {
1387
        $tags = (string)$this->stdWrapValue('addPageCacheTags', $conf ?? []);
1388
        if (!empty($tags)) {
1389
            $cacheTags = GeneralUtility::trimExplode(',', $tags, true);
1390
            $this->getTypoScriptFrontendController()->addCacheTags($cacheTags);
1391
        }
1392
        return $content;
1393
    }
1394
1395
    /**
1396
     * setContentToCurrent
1397
     * actually it just does the contrary: Sets the value of 'current' based on current content
1398
     *
1399
     * @param string $content Input value undergoing processing in this function.
1400
     * @return string The processed input value
1401
     */
1402
    public function stdWrap_setContentToCurrent($content = '')
1403
    {
1404
        $this->data[$this->currentValKey] = $content;
1405
        return $content;
1406
    }
1407
1408
    /**
1409
     * setCurrent
1410
     * Sets the value of 'current' based on the outcome of stdWrap operations
1411
     *
1412
     * @param string $content Input value undergoing processing in this function.
1413
     * @param array $conf stdWrap properties for setCurrent.
1414
     * @return string The processed input value
1415
     */
1416
    public function stdWrap_setCurrent($content = '', $conf = [])
1417
    {
1418
        $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null;
1419
        return $content;
1420
    }
1421
1422
    /**
1423
     * lang
1424
     * Translates content based on the language currently used by the FE
1425
     *
1426
     * @param string $content Input value undergoing processing in this function.
1427
     * @param array $conf stdWrap properties for lang.
1428
     * @return string The processed input value
1429
     */
1430
    public function stdWrap_lang($content = '', $conf = [])
1431
    {
1432
        $currentLanguageCode = $this->getTypoScriptFrontendController()->getLanguage()->getTypo3Language();
1433
        if ($currentLanguageCode && isset($conf['lang.'][$currentLanguageCode])) {
1434
            $content = $conf['lang.'][$currentLanguageCode];
1435
        }
1436
        return $content;
1437
    }
1438
1439
    /**
1440
     * data
1441
     * Gets content from different sources based on getText functions, makes use of alternativeData, when set
1442
     *
1443
     * @param string $content Input value undergoing processing in this function.
1444
     * @param array $conf stdWrap properties for data.
1445
     * @return string The processed input value
1446
     */
1447
    public function stdWrap_data($content = '', $conf = [])
1448
    {
1449
        $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...
1450
        // This must be unset directly after
1451
        $this->alternativeData = '';
1452
        return $content;
1453
    }
1454
1455
    /**
1456
     * field
1457
     * Gets content from a DB field
1458
     *
1459
     * @param string $content Input value undergoing processing in this function.
1460
     * @param array $conf stdWrap properties for field.
1461
     * @return string The processed input value
1462
     */
1463
    public function stdWrap_field($content = '', $conf = [])
1464
    {
1465
        return $this->getFieldVal($conf['field']);
1466
    }
1467
1468
    /**
1469
     * current
1470
     * Gets content that has been previously set as 'current'
1471
     * Can be set via setContentToCurrent or setCurrent or will be set automatically i.e. inside the split function
1472
     *
1473
     * @param string $content Input value undergoing processing in this function.
1474
     * @param array $conf stdWrap properties for current.
1475
     * @return string The processed input value
1476
     */
1477
    public function stdWrap_current($content = '', $conf = [])
1478
    {
1479
        return $this->data[$this->currentValKey];
1480
    }
1481
1482
    /**
1483
     * cObject
1484
     * Will replace the content with the value of an official TypoScript cObject
1485
     * like TEXT, COA, HMENU
1486
     *
1487
     * @param string $content Input value undergoing processing in this function.
1488
     * @param array $conf stdWrap properties for cObject.
1489
     * @return string The processed input value
1490
     */
1491
    public function stdWrap_cObject($content = '', $conf = [])
1492
    {
1493
        return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject');
1494
    }
1495
1496
    /**
1497
     * numRows
1498
     * Counts the number of returned records of a DB operation
1499
     * makes use of select internally
1500
     *
1501
     * @param string $content Input value undergoing processing in this function.
1502
     * @param array $conf stdWrap properties for numRows.
1503
     * @return string The processed input value
1504
     */
1505
    public function stdWrap_numRows($content = '', $conf = [])
1506
    {
1507
        return $this->numRows($conf['numRows.']);
1508
    }
1509
1510
    /**
1511
     * preUserFunc
1512
     * Will execute a user public function before the content will be modified by any other stdWrap function
1513
     *
1514
     * @param string $content Input value undergoing processing in this function.
1515
     * @param array $conf stdWrap properties for preUserFunc.
1516
     * @return string The processed input value
1517
     */
1518
    public function stdWrap_preUserFunc($content = '', $conf = [])
1519
    {
1520
        return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'], $content);
1521
    }
1522
1523
    /**
1524
     * stdWrap override hook
1525
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1526
     * this hook will execute functions on existing content but still before the content gets modified or replaced
1527
     *
1528
     * @param string $content Input value undergoing processing in these functions.
1529
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1530
     * @return string The processed input value
1531
     */
1532
    public function stdWrap_stdWrapOverride($content = '', $conf = [])
1533
    {
1534
        foreach ($this->stdWrapHookObjects as $hookObject) {
1535
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1536
            $content = $hookObject->stdWrapOverride($content, $conf, $this);
1537
        }
1538
        return $content;
1539
    }
1540
1541
    /**
1542
     * override
1543
     * Will override the current value of content with its own value'
1544
     *
1545
     * @param string $content Input value undergoing processing in this function.
1546
     * @param array $conf stdWrap properties for override.
1547
     * @return string The processed input value
1548
     */
1549
    public function stdWrap_override($content = '', $conf = [])
1550
    {
1551
        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 $str 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

1551
        if (trim(/** @scrutinizer ignore-type */ $conf['override'] ?? false)) {
Loading history...
1552
            $content = $conf['override'];
1553
        }
1554
        return $content;
1555
    }
1556
1557
    /**
1558
     * preIfEmptyListNum
1559
     * Gets a value off a CSV list before the following ifEmpty check
1560
     * 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
1561
     *
1562
     * @param string $content Input value undergoing processing in this function.
1563
     * @param array $conf stdWrap properties for preIfEmptyListNum.
1564
     * @return string The processed input value
1565
     */
1566
    public function stdWrap_preIfEmptyListNum($content = '', $conf = [])
1567
    {
1568
        return $this->listNum($content, $conf['preIfEmptyListNum'] ?? null, $conf['preIfEmptyListNum.']['splitChar'] ?? null);
1569
    }
1570
1571
    /**
1572
     * ifNull
1573
     * Will set content to a replacement value in case the value of content is NULL
1574
     *
1575
     * @param string|null $content Input value undergoing processing in this function.
1576
     * @param array $conf stdWrap properties for ifNull.
1577
     * @return string The processed input value
1578
     */
1579
    public function stdWrap_ifNull($content = '', $conf = [])
1580
    {
1581
        return $content ?? $conf['ifNull'];
1582
    }
1583
1584
    /**
1585
     * ifEmpty
1586
     * Will set content to a replacement value in case the trimmed value of content returns FALSE
1587
     * 0 (zero) will be replaced as well
1588
     *
1589
     * @param string $content Input value undergoing processing in this function.
1590
     * @param array $conf stdWrap properties for ifEmpty.
1591
     * @return string The processed input value
1592
     */
1593
    public function stdWrap_ifEmpty($content = '', $conf = [])
1594
    {
1595
        if (!trim($content)) {
1596
            $content = $conf['ifEmpty'];
1597
        }
1598
        return $content;
1599
    }
1600
1601
    /**
1602
     * ifBlank
1603
     * Will set content to a replacement value in case the trimmed value of content has no length
1604
     * 0 (zero) will not be replaced
1605
     *
1606
     * @param string $content Input value undergoing processing in this function.
1607
     * @param array $conf stdWrap properties for ifBlank.
1608
     * @return string The processed input value
1609
     */
1610
    public function stdWrap_ifBlank($content = '', $conf = [])
1611
    {
1612
        if (trim($content) === '') {
1613
            $content = $conf['ifBlank'];
1614
        }
1615
        return $content;
1616
    }
1617
1618
    /**
1619
     * listNum
1620
     * Gets a value off a CSV list after ifEmpty check
1621
     * Might return an empty value in case the CSV does not contain a value at the position given by listNum
1622
     * Use preIfEmptyListNum to avoid that behaviour
1623
     *
1624
     * @param string $content Input value undergoing processing in this function.
1625
     * @param array $conf stdWrap properties for listNum.
1626
     * @return string The processed input value
1627
     */
1628
    public function stdWrap_listNum($content = '', $conf = [])
1629
    {
1630
        return $this->listNum($content, $conf['listNum'] ?? null, $conf['listNum.']['splitChar'] ?? null);
1631
    }
1632
1633
    /**
1634
     * trim
1635
     * Cuts off any whitespace at the beginning and the end of the content
1636
     *
1637
     * @param string $content Input value undergoing processing in this function.
1638
     * @return string The processed input value
1639
     */
1640
    public function stdWrap_trim($content = '')
1641
    {
1642
        return trim($content);
1643
    }
1644
1645
    /**
1646
     * strPad
1647
     * Will return a string padded left/right/on both sides, based on configuration given as stdWrap properties
1648
     *
1649
     * @param string $content Input value undergoing processing in this function.
1650
     * @param array $conf stdWrap properties for strPad.
1651
     * @return string The processed input value
1652
     */
1653
    public function stdWrap_strPad($content = '', $conf = [])
1654
    {
1655
        // Must specify a length in conf for this to make sense
1656
        $length = (int)$this->stdWrapValue('length', $conf['strPad.'] ?? [], 0);
1657
        // Padding with space is PHP-default
1658
        $padWith = (string)$this->stdWrapValue('padWith', $conf['strPad.'] ?? [], ' ');
1659
        // Padding on the right side is PHP-default
1660
        $padType = STR_PAD_RIGHT;
1661
1662
        if (!empty($conf['strPad.']['type'])) {
1663
            $type = (string)$this->stdWrapValue('type', $conf['strPad.'] ?? []);
1664
            if (strtolower($type) === 'left') {
1665
                $padType = STR_PAD_LEFT;
1666
            } elseif (strtolower($type) === 'both') {
1667
                $padType = STR_PAD_BOTH;
1668
            }
1669
        }
1670
        return str_pad($content, $length, $padWith, $padType);
1671
    }
1672
1673
    /**
1674
     * stdWrap
1675
     * A recursive call of the stdWrap function set
1676
     * This enables the user to execute stdWrap functions in another than the predefined order
1677
     * It modifies the content, not the property
1678
     * while the new feature of chained stdWrap functions modifies the property and not the content
1679
     *
1680
     * @param string $content Input value undergoing processing in this function.
1681
     * @param array $conf stdWrap properties for stdWrap.
1682
     * @return string The processed input value
1683
     */
1684
    public function stdWrap_stdWrap($content = '', $conf = [])
1685
    {
1686
        return $this->stdWrap($content, $conf['stdWrap.']);
1687
    }
1688
1689
    /**
1690
     * stdWrap process hook
1691
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1692
     * this hook executes functions directly after the recursive stdWrap function call but still before the content gets modified
1693
     *
1694
     * @param string $content Input value undergoing processing in these functions.
1695
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1696
     * @return string The processed input value
1697
     */
1698
    public function stdWrap_stdWrapProcess($content = '', $conf = [])
1699
    {
1700
        foreach ($this->stdWrapHookObjects as $hookObject) {
1701
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1702
            $content = $hookObject->stdWrapProcess($content, $conf, $this);
1703
        }
1704
        return $content;
1705
    }
1706
1707
    /**
1708
     * required
1709
     * Will immediately stop rendering and return an empty value
1710
     * when there is no content at this point
1711
     *
1712
     * @param string $content Input value undergoing processing in this function.
1713
     * @return string The processed input value
1714
     */
1715
    public function stdWrap_required($content = '')
1716
    {
1717
        if ((string)$content === '') {
1718
            $content = '';
1719
            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1720
        }
1721
        return $content;
1722
    }
1723
1724
    /**
1725
     * if
1726
     * Will immediately stop rendering and return an empty value
1727
     * when the result of the checks returns FALSE
1728
     *
1729
     * @param string $content Input value undergoing processing in this function.
1730
     * @param array $conf stdWrap properties for if.
1731
     * @return string The processed input value
1732
     */
1733
    public function stdWrap_if($content = '', $conf = [])
1734
    {
1735
        if (empty($conf['if.']) || $this->checkIf($conf['if.'])) {
1736
            return $content;
1737
        }
1738
        $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1739
        return '';
1740
    }
1741
1742
    /**
1743
     * fieldRequired
1744
     * Will immediately stop rendering and return an empty value
1745
     * when there is no content in the field given by fieldRequired
1746
     *
1747
     * @param string $content Input value undergoing processing in this function.
1748
     * @param array $conf stdWrap properties for fieldRequired.
1749
     * @return string The processed input value
1750
     */
1751
    public function stdWrap_fieldRequired($content = '', $conf = [])
1752
    {
1753
        if (!trim($this->data[$conf['fieldRequired'] ?? null] ?? '')) {
1754
            $content = '';
1755
            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1756
        }
1757
        return $content;
1758
    }
1759
1760
    /**
1761
     * stdWrap csConv: Converts the input to UTF-8
1762
     *
1763
     * The character set of the input must be specified. Returns the input if
1764
     * matters go wrong, for example if an invalid character set is given.
1765
     *
1766
     * @param string $content The string to convert.
1767
     * @param array $conf stdWrap properties for csConv.
1768
     * @return string The processed input.
1769
     */
1770
    public function stdWrap_csConv($content = '', $conf = [])
1771
    {
1772
        if (!empty($conf['csConv'])) {
1773
            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv'])));
1774
            return $output !== false && $output !== '' ? $output : $content;
1775
        }
1776
        return $content;
1777
    }
1778
1779
    /**
1780
     * parseFunc
1781
     * Will parse the content based on functions given as stdWrap properties
1782
     * Heavily used together with RTE based content
1783
     *
1784
     * @param string $content Input value undergoing processing in this function.
1785
     * @param array $conf stdWrap properties for parseFunc.
1786
     * @return string The processed input value
1787
     */
1788
    public function stdWrap_parseFunc($content = '', $conf = [])
1789
    {
1790
        return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']);
1791
    }
1792
1793
    /**
1794
     * HTMLparser
1795
     * Will parse HTML content based on functions given as stdWrap properties
1796
     * Heavily used together with RTE based content
1797
     *
1798
     * @param string $content Input value undergoing processing in this function.
1799
     * @param array $conf stdWrap properties for HTMLparser.
1800
     * @return string The processed input value
1801
     */
1802
    public function stdWrap_HTMLparser($content = '', $conf = [])
1803
    {
1804
        if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) {
1805
            $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']);
1806
        }
1807
        return $content;
1808
    }
1809
1810
    /**
1811
     * split
1812
     * Will split the content by a given token and treat the results separately
1813
     * Automatically fills 'current' with a single result
1814
     *
1815
     * @param string $content Input value undergoing processing in this function.
1816
     * @param array $conf stdWrap properties for split.
1817
     * @return string The processed input value
1818
     */
1819
    public function stdWrap_split($content = '', $conf = [])
1820
    {
1821
        return $this->splitObj($content, $conf['split.']);
1822
    }
1823
1824
    /**
1825
     * replacement
1826
     * Will execute replacements on the content (optionally with preg-regex)
1827
     *
1828
     * @param string $content Input value undergoing processing in this function.
1829
     * @param array $conf stdWrap properties for replacement.
1830
     * @return string The processed input value
1831
     */
1832
    public function stdWrap_replacement($content = '', $conf = [])
1833
    {
1834
        return $this->replacement($content, $conf['replacement.']);
1835
    }
1836
1837
    /**
1838
     * prioriCalc
1839
     * Will use the content as a mathematical term and calculate the result
1840
     * Can be set to 1 to just get a calculated value or 'intval' to get the integer of the result
1841
     *
1842
     * @param string $content Input value undergoing processing in this function.
1843
     * @param array $conf stdWrap properties for prioriCalc.
1844
     * @return string The processed input value
1845
     */
1846
    public function stdWrap_prioriCalc($content = '', $conf = [])
1847
    {
1848
        $content = MathUtility::calculateWithParentheses($content);
1849
        if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') {
1850
            $content = (int)$content;
1851
        }
1852
        return $content;
1853
    }
1854
1855
    /**
1856
     * char
1857
     * Returns a one-character string containing the character specified by ascii code.
1858
     *
1859
     * Reliable results only for character codes in the integer range 0 - 127.
1860
     *
1861
     * @see https://php.net/manual/en/function.chr.php
1862
     * @param string $content Input value undergoing processing in this function.
1863
     * @param array $conf stdWrap properties for char.
1864
     * @return string The processed input value
1865
     */
1866
    public function stdWrap_char($content = '', $conf = [])
1867
    {
1868
        return chr((int)$conf['char']);
1869
    }
1870
1871
    /**
1872
     * intval
1873
     * Will return an integer value of the current content
1874
     *
1875
     * @param string $content Input value undergoing processing in this function.
1876
     * @return string The processed input value
1877
     */
1878
    public function stdWrap_intval($content = '')
1879
    {
1880
        return (int)$content;
1881
    }
1882
1883
    /**
1884
     * Will return a hashed value of the current content
1885
     *
1886
     * @param string $content Input value undergoing processing in this function.
1887
     * @param array $conf stdWrap properties for hash.
1888
     * @return string The processed input value
1889
     * @link https://php.net/manual/de/function.hash-algos.php for a list of supported hash algorithms
1890
     */
1891
    public function stdWrap_hash($content = '', array $conf = [])
1892
    {
1893
        $algorithm = (string)$this->stdWrapValue('hash', $conf ?? []);
1894
        if (function_exists('hash') && in_array($algorithm, hash_algos())) {
1895
            return hash($algorithm, $content);
1896
        }
1897
        // Non-existing hashing algorithm
1898
        return '';
1899
    }
1900
1901
    /**
1902
     * stdWrap_round will return a rounded number with ceil(), floor() or round(), defaults to round()
1903
     * Only the english number format is supported . (dot) as decimal point
1904
     *
1905
     * @param string $content Input value undergoing processing in this function.
1906
     * @param array $conf stdWrap properties for round.
1907
     * @return string The processed input value
1908
     */
1909
    public function stdWrap_round($content = '', $conf = [])
1910
    {
1911
        return $this->round($content, $conf['round.']);
1912
    }
1913
1914
    /**
1915
     * numberFormat
1916
     * Will return a formatted number based on configuration given as stdWrap properties
1917
     *
1918
     * @param string $content Input value undergoing processing in this function.
1919
     * @param array $conf stdWrap properties for numberFormat.
1920
     * @return string The processed input value
1921
     */
1922
    public function stdWrap_numberFormat($content = '', $conf = [])
1923
    {
1924
        return $this->numberFormat((float)$content, $conf['numberFormat.'] ?? []);
1925
    }
1926
1927
    /**
1928
     * expandList
1929
     * Will return a formatted number based on configuration given as stdWrap properties
1930
     *
1931
     * @param string $content Input value undergoing processing in this function.
1932
     * @return string The processed input value
1933
     */
1934
    public function stdWrap_expandList($content = '')
1935
    {
1936
        return GeneralUtility::expandList($content);
1937
    }
1938
1939
    /**
1940
     * date
1941
     * Will return a formatted date based on configuration given according to PHP date/gmdate properties
1942
     * Will return gmdate when the property GMT returns TRUE
1943
     *
1944
     * @param string $content Input value undergoing processing in this function.
1945
     * @param array $conf stdWrap properties for date.
1946
     * @return string The processed input value
1947
     */
1948
    public function stdWrap_date($content = '', $conf = [])
1949
    {
1950
        // Check for zero length string to mimic default case of date/gmdate.
1951
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
1952
        $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
1953
        return $content;
1954
    }
1955
1956
    /**
1957
     * strftime
1958
     * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties
1959
     * Will return gmstrftime when the property GMT returns TRUE
1960
     *
1961
     * @param string $content Input value undergoing processing in this function.
1962
     * @param array $conf stdWrap properties for strftime.
1963
     * @return string The processed input value
1964
     */
1965
    public function stdWrap_strftime($content = '', $conf = [])
1966
    {
1967
        // Check for zero length string to mimic default case of strtime/gmstrftime
1968
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
1969
        $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT'])
1970
            ? gmstrftime($conf['strftime'] ?? null, $content)
1971
            : strftime($conf['strftime'] ?? null, $content);
1972
        if (!empty($conf['strftime.']['charset'])) {
1973
            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['strftime.']['charset'])));
1974
            return $output ?: $content;
1975
        }
1976
        return $content;
1977
    }
1978
1979
    /**
1980
     * strtotime
1981
     * Will return a timestamp based on configuration given according to PHP strtotime
1982
     *
1983
     * @param string $content Input value undergoing processing in this function.
1984
     * @param array $conf stdWrap properties for strtotime.
1985
     * @return string The processed input value
1986
     */
1987
    public function stdWrap_strtotime($content = '', $conf = [])
1988
    {
1989
        if ($conf['strtotime'] !== '1') {
1990
            $content .= ' ' . $conf['strtotime'];
1991
        }
1992
        return strtotime($content, $GLOBALS['EXEC_TIME']);
1993
    }
1994
1995
    /**
1996
     * age
1997
     * Will return the age of a given timestamp based on configuration given by stdWrap properties
1998
     *
1999
     * @param string $content Input value undergoing processing in this function.
2000
     * @param array $conf stdWrap properties for age.
2001
     * @return string The processed input value
2002
     */
2003
    public function stdWrap_age($content = '', $conf = [])
2004
    {
2005
        return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null);
2006
    }
2007
2008
    /**
2009
     * case
2010
     * Will transform the content to be upper or lower case only
2011
     * Leaves HTML tags untouched
2012
     *
2013
     * @param string $content Input value undergoing processing in this function.
2014
     * @param array $conf stdWrap properties for case.
2015
     * @return string The processed input value
2016
     */
2017
    public function stdWrap_case($content = '', $conf = [])
2018
    {
2019
        return $this->HTMLcaseshift($content, $conf['case']);
2020
    }
2021
2022
    /**
2023
     * bytes
2024
     * Will return the size of a given number in Bytes	 *
2025
     *
2026
     * @param string $content Input value undergoing processing in this function.
2027
     * @param array $conf stdWrap properties for bytes.
2028
     * @return string The processed input value
2029
     */
2030
    public function stdWrap_bytes($content = '', $conf = [])
2031
    {
2032
        return GeneralUtility::formatSize((int)$content, $conf['bytes.']['labels'], $conf['bytes.']['base']);
2033
    }
2034
2035
    /**
2036
     * substring
2037
     * Will return a substring based on position information given by stdWrap properties
2038
     *
2039
     * @param string $content Input value undergoing processing in this function.
2040
     * @param array $conf stdWrap properties for substring.
2041
     * @return string The processed input value
2042
     */
2043
    public function stdWrap_substring($content = '', $conf = [])
2044
    {
2045
        return $this->substring($content, $conf['substring']);
2046
    }
2047
2048
    /**
2049
     * cropHTML
2050
     * Crops content to a given size while leaving HTML tags untouched
2051
     *
2052
     * @param string $content Input value undergoing processing in this function.
2053
     * @param array $conf stdWrap properties for cropHTML.
2054
     * @return string The processed input value
2055
     */
2056
    public function stdWrap_cropHTML($content = '', $conf = [])
2057
    {
2058
        return $this->cropHTML($content, $conf['cropHTML'] ?? '');
2059
    }
2060
2061
    /**
2062
     * stripHtml
2063
     * Completely removes HTML tags from content
2064
     *
2065
     * @param string $content Input value undergoing processing in this function.
2066
     * @return string The processed input value
2067
     */
2068
    public function stdWrap_stripHtml($content = '')
2069
    {
2070
        return strip_tags($content);
2071
    }
2072
2073
    /**
2074
     * crop
2075
     * Crops content to a given size without caring about HTML tags
2076
     *
2077
     * @param string $content Input value undergoing processing in this function.
2078
     * @param array $conf stdWrap properties for crop.
2079
     * @return string The processed input value
2080
     */
2081
    public function stdWrap_crop($content = '', $conf = [])
2082
    {
2083
        return $this->crop($content, $conf['crop']);
2084
    }
2085
2086
    /**
2087
     * rawUrlEncode
2088
     * Encodes content to be used within URLs
2089
     *
2090
     * @param string $content Input value undergoing processing in this function.
2091
     * @return string The processed input value
2092
     */
2093
    public function stdWrap_rawUrlEncode($content = '')
2094
    {
2095
        return rawurlencode($content);
2096
    }
2097
2098
    /**
2099
     * htmlSpecialChars
2100
     * Transforms HTML tags to readable text by replacing special characters with their HTML entity
2101
     * When preserveEntities returns TRUE, existing entities will be left untouched
2102
     *
2103
     * @param string $content Input value undergoing processing in this function.
2104
     * @param array $conf stdWrap properties for htmlSpecialChars.
2105
     * @return string The processed input value
2106
     */
2107
    public function stdWrap_htmlSpecialChars($content = '', $conf = [])
2108
    {
2109
        if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) {
2110
            $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false);
2111
        } else {
2112
            $content = htmlspecialchars($content);
2113
        }
2114
        return $content;
2115
    }
2116
2117
    /**
2118
     * encodeForJavaScriptValue
2119
     * Escapes content to be used inside JavaScript strings. Single quotes are added around the value.
2120
     *
2121
     * @param string $content Input value undergoing processing in this function
2122
     * @return string The processed input value
2123
     */
2124
    public function stdWrap_encodeForJavaScriptValue($content = '')
2125
    {
2126
        return GeneralUtility::quoteJSvalue($content);
2127
    }
2128
2129
    /**
2130
     * doubleBrTag
2131
     * Searches for double line breaks and replaces them with the given value
2132
     *
2133
     * @param string $content Input value undergoing processing in this function.
2134
     * @param array $conf stdWrap properties for doubleBrTag.
2135
     * @return string The processed input value
2136
     */
2137
    public function stdWrap_doubleBrTag($content = '', $conf = [])
2138
    {
2139
        return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? null, $content);
2140
    }
2141
2142
    /**
2143
     * br
2144
     * Searches for single line breaks and replaces them with a <br />/<br> tag
2145
     * according to the doctype
2146
     *
2147
     * @param string $content Input value undergoing processing in this function.
2148
     * @return string The processed input value
2149
     */
2150
    public function stdWrap_br($content = '')
2151
    {
2152
        return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype));
2153
    }
2154
2155
    /**
2156
     * brTag
2157
     * Searches for single line feeds and replaces them with the given value
2158
     *
2159
     * @param string $content Input value undergoing processing in this function.
2160
     * @param array $conf stdWrap properties for brTag.
2161
     * @return string The processed input value
2162
     */
2163
    public function stdWrap_brTag($content = '', $conf = [])
2164
    {
2165
        return str_replace(LF, $conf['brTag'] ?? null, $content);
2166
    }
2167
2168
    /**
2169
     * encapsLines
2170
     * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet
2171
     * and wrapping them with values given by stdWrap properties
2172
     *
2173
     * @param string $content Input value undergoing processing in this function.
2174
     * @param array $conf stdWrap properties for erncapsLines.
2175
     * @return string The processed input value
2176
     */
2177
    public function stdWrap_encapsLines($content = '', $conf = [])
2178
    {
2179
        return $this->encaps_lineSplit($content, $conf['encapsLines.']);
2180
    }
2181
2182
    /**
2183
     * keywords
2184
     * Transforms content into a CSV list to be used i.e. as keywords within a meta tag
2185
     *
2186
     * @param string $content Input value undergoing processing in this function.
2187
     * @return string The processed input value
2188
     */
2189
    public function stdWrap_keywords($content = '')
2190
    {
2191
        return $this->keywords($content);
2192
    }
2193
2194
    /**
2195
     * innerWrap
2196
     * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2197
     * See wrap
2198
     *
2199
     * @param string $content Input value undergoing processing in this function.
2200
     * @param array $conf stdWrap properties for innerWrap.
2201
     * @return string The processed input value
2202
     */
2203
    public function stdWrap_innerWrap($content = '', $conf = [])
2204
    {
2205
        return $this->wrap($content, $conf['innerWrap'] ?? null);
2206
    }
2207
2208
    /**
2209
     * innerWrap2
2210
     * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2211
     * See wrap
2212
     *
2213
     * @param string $content Input value undergoing processing in this function.
2214
     * @param array $conf stdWrap properties for innerWrap2.
2215
     * @return string The processed input value
2216
     */
2217
    public function stdWrap_innerWrap2($content = '', $conf = [])
2218
    {
2219
        return $this->wrap($content, $conf['innerWrap2'] ?? null);
2220
    }
2221
2222
    /**
2223
     * preCObject
2224
     * A content object that is prepended to the current content but between the innerWraps and the rest of the wraps
2225
     *
2226
     * @param string $content Input value undergoing processing in this function.
2227
     * @param array $conf stdWrap properties for preCObject.
2228
     * @return string The processed input value
2229
     */
2230
    public function stdWrap_preCObject($content = '', $conf = [])
2231
    {
2232
        return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content;
2233
    }
2234
2235
    /**
2236
     * postCObject
2237
     * A content object that is appended to the current content but between the innerWraps and the rest of the wraps
2238
     *
2239
     * @param string $content Input value undergoing processing in this function.
2240
     * @param array $conf stdWrap properties for postCObject.
2241
     * @return string The processed input value
2242
     */
2243
    public function stdWrap_postCObject($content = '', $conf = [])
2244
    {
2245
        return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject');
2246
    }
2247
2248
    /**
2249
     * wrapAlign
2250
     * Wraps content with a div container having the style attribute text-align set to the given value
2251
     * See wrap
2252
     *
2253
     * @param string $content Input value undergoing processing in this function.
2254
     * @param array $conf stdWrap properties for wrapAlign.
2255
     * @return string The processed input value
2256
     */
2257
    public function stdWrap_wrapAlign($content = '', $conf = [])
2258
    {
2259
        $wrapAlign = trim($conf['wrapAlign'] ?? '');
2260
        if ($wrapAlign) {
2261
            $content = $this->wrap($content, '<div style="text-align:' . htmlspecialchars($wrapAlign) . ';">|</div>');
2262
        }
2263
        return $content;
2264
    }
2265
2266
    /**
2267
     * typolink
2268
     * Wraps the content with a link tag
2269
     * URLs and other attributes are created automatically by the values given in the stdWrap properties
2270
     * See wrap
2271
     *
2272
     * @param string $content Input value undergoing processing in this function.
2273
     * @param array $conf stdWrap properties for typolink.
2274
     * @return string The processed input value
2275
     */
2276
    public function stdWrap_typolink($content = '', $conf = [])
2277
    {
2278
        return $this->typoLink($content, $conf['typolink.']);
2279
    }
2280
2281
    /**
2282
     * wrap
2283
     * This is the "mother" of all wraps
2284
     * Third of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2285
     * Basically it will put additional content before and after the current content using a split character as a placeholder for the current content
2286
     * The default split character is | but it can be replaced with other characters by the property splitChar
2287
     * Any other wrap that does not have own splitChar settings will be using the default split char though
2288
     *
2289
     * @param string $content Input value undergoing processing in this function.
2290
     * @param array $conf stdWrap properties for wrap.
2291
     * @return string The processed input value
2292
     */
2293
    public function stdWrap_wrap($content = '', $conf = [])
2294
    {
2295
        return $this->wrap(
2296
            $content,
2297
            $conf['wrap'] ?? null,
2298
            $conf['wrap.']['splitChar'] ?? '|'
2299
        );
2300
    }
2301
2302
    /**
2303
     * noTrimWrap
2304
     * Fourth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2305
     * The major difference to any other wrap is, that this one can make use of whitespace without trimming	 *
2306
     *
2307
     * @param string $content Input value undergoing processing in this function.
2308
     * @param array $conf stdWrap properties for noTrimWrap.
2309
     * @return string The processed input value
2310
     */
2311
    public function stdWrap_noTrimWrap($content = '', $conf = [])
2312
    {
2313
        $splitChar = isset($conf['noTrimWrap.']['splitChar.'])
2314
            ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.'])
2315
            : $conf['noTrimWrap.']['splitChar'] ?? '';
2316
        if ($splitChar === null || $splitChar === '') {
2317
            $splitChar = '|';
2318
        }
2319
        $content = $this->noTrimWrap(
2320
            $content,
2321
            $conf['noTrimWrap'],
2322
            $splitChar
2323
        );
2324
        return $content;
2325
    }
2326
2327
    /**
2328
     * wrap2
2329
     * Fifth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2330
     * The default split character is | but it can be replaced with other characters by the property splitChar
2331
     *
2332
     * @param string $content Input value undergoing processing in this function.
2333
     * @param array $conf stdWrap properties for wrap2.
2334
     * @return string The processed input value
2335
     */
2336
    public function stdWrap_wrap2($content = '', $conf = [])
2337
    {
2338
        return $this->wrap(
2339
            $content,
2340
            $conf['wrap2'] ?? null,
2341
            $conf['wrap2.']['splitChar'] ?? '|'
2342
        );
2343
    }
2344
2345
    /**
2346
     * dataWrap
2347
     * Sixth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2348
     * 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
2349
     *
2350
     * @param string $content Input value undergoing processing in this function.
2351
     * @param array $conf stdWrap properties for dataWrap.
2352
     * @return string The processed input value
2353
     */
2354
    public function stdWrap_dataWrap($content = '', $conf = [])
2355
    {
2356
        return $this->dataWrap($content, $conf['dataWrap']);
2357
    }
2358
2359
    /**
2360
     * prepend
2361
     * A content object that will be prepended to the current content after most of the wraps have already been applied
2362
     *
2363
     * @param string $content Input value undergoing processing in this function.
2364
     * @param array $conf stdWrap properties for prepend.
2365
     * @return string The processed input value
2366
     */
2367
    public function stdWrap_prepend($content = '', $conf = [])
2368
    {
2369
        return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content;
2370
    }
2371
2372
    /**
2373
     * append
2374
     * A content object that will be appended to the current content after most of the wraps have already been applied
2375
     *
2376
     * @param string $content Input value undergoing processing in this function.
2377
     * @param array $conf stdWrap properties for append.
2378
     * @return string The processed input value
2379
     */
2380
    public function stdWrap_append($content = '', $conf = [])
2381
    {
2382
        return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append');
2383
    }
2384
2385
    /**
2386
     * wrap3
2387
     * Seventh of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2388
     * The default split character is | but it can be replaced with other characters by the property splitChar
2389
     *
2390
     * @param string $content Input value undergoing processing in this function.
2391
     * @param array $conf stdWrap properties for wrap3.
2392
     * @return string The processed input value
2393
     */
2394
    public function stdWrap_wrap3($content = '', $conf = [])
2395
    {
2396
        return $this->wrap(
2397
            $content,
2398
            $conf['wrap3'] ?? null,
2399
            $conf['wrap3.']['splitChar'] ?? '|'
2400
        );
2401
    }
2402
2403
    /**
2404
     * orderedStdWrap
2405
     * Calls stdWrap for each entry in the provided array
2406
     *
2407
     * @param string $content Input value undergoing processing in this function.
2408
     * @param array $conf stdWrap properties for orderedStdWrap.
2409
     * @return string The processed input value
2410
     */
2411
    public function stdWrap_orderedStdWrap($content = '', $conf = [])
2412
    {
2413
        $sortedKeysArray = ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true);
2414
        foreach ($sortedKeysArray as $key) {
2415
            $content = $this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null);
2416
        }
2417
        return $content;
2418
    }
2419
2420
    /**
2421
     * outerWrap
2422
     * Eighth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2423
     *
2424
     * @param string $content Input value undergoing processing in this function.
2425
     * @param array $conf stdWrap properties for outerWrap.
2426
     * @return string The processed input value
2427
     */
2428
    public function stdWrap_outerWrap($content = '', $conf = [])
2429
    {
2430
        return $this->wrap($content, $conf['outerWrap'] ?? null);
2431
    }
2432
2433
    /**
2434
     * insertData
2435
     * Can fetch additional content the same way data does and replaces any occurrence of {field:whatever} with this content
2436
     *
2437
     * @param string $content Input value undergoing processing in this function.
2438
     * @return string The processed input value
2439
     */
2440
    public function stdWrap_insertData($content = '')
2441
    {
2442
        return $this->insertData($content);
2443
    }
2444
2445
    /**
2446
     * postUserFunc
2447
     * Will execute a user function after the content has been modified by any other stdWrap function
2448
     *
2449
     * @param string $content Input value undergoing processing in this function.
2450
     * @param array $conf stdWrap properties for postUserFunc.
2451
     * @return string The processed input value
2452
     */
2453
    public function stdWrap_postUserFunc($content = '', $conf = [])
2454
    {
2455
        return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'], $content);
2456
    }
2457
2458
    /**
2459
     * postUserFuncInt
2460
     * Will execute a user function after the content has been created and each time it is fetched from Cache
2461
     * The result of this function itself will not be cached
2462
     *
2463
     * @param string $content Input value undergoing processing in this function.
2464
     * @param array $conf stdWrap properties for postUserFuncInt.
2465
     * @return string The processed input value
2466
     */
2467
    public function stdWrap_postUserFuncInt($content = '', $conf = [])
2468
    {
2469
        $substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash();
2470
        $this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [
2471
            'content' => $content,
2472
            'postUserFunc' => $conf['postUserFuncInt'],
2473
            'conf' => $conf['postUserFuncInt.'],
2474
            'type' => 'POSTUSERFUNC',
2475
            'cObj' => serialize($this)
2476
        ];
2477
        $content = '<!--' . $substKey . '-->';
2478
        return $content;
2479
    }
2480
2481
    /**
2482
     * prefixComment
2483
     * Will add HTML comments to the content to make it easier to identify certain content elements within the HTML output later on
2484
     *
2485
     * @param string $content Input value undergoing processing in this function.
2486
     * @param array $conf stdWrap properties for prefixComment.
2487
     * @return string The processed input value
2488
     */
2489
    public function stdWrap_prefixComment($content = '', $conf = [])
2490
    {
2491
        if (
2492
            (!isset($this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) || !$this->getTypoScriptFrontendController()->config['config']['disablePrefixComment'])
2493
            && !empty($conf['prefixComment'])
2494
        ) {
2495
            $content = $this->prefixComment($conf['prefixComment'], [], $content);
2496
        }
2497
        return $content;
2498
    }
2499
2500
    /**
2501
     * editIcons
2502
     * Will render icons for frontend editing as long as there is a BE user logged in
2503
     *
2504
     * @param string $content Input value undergoing processing in this function.
2505
     * @param array $conf stdWrap properties for editIcons.
2506
     * @return string The processed input value
2507
     */
2508
    public function stdWrap_editIcons($content = '', $conf = [])
2509
    {
2510
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn() && $conf['editIcons']) {
2511
            if (!isset($conf['editIcons.']) || !is_array($conf['editIcons.'])) {
2512
                $conf['editIcons.'] = [];
2513
            }
2514
            $content = $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']);
2515
        }
2516
        return $content;
2517
    }
2518
2519
    /**
2520
     * editPanel
2521
     * Will render the edit panel for frontend editing as long as there is a BE user logged in
2522
     *
2523
     * @param string $content Input value undergoing processing in this function.
2524
     * @param array $conf stdWrap properties for editPanel.
2525
     * @return string The processed input value
2526
     */
2527
    public function stdWrap_editPanel($content = '', $conf = [])
2528
    {
2529
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
2530
            $content = $this->editPanel($content, $conf['editPanel.']);
2531
        }
2532
        return $content;
2533
    }
2534
2535
    /**
2536
     * Store content into cache
2537
     *
2538
     * @param string $content Input value undergoing processing in these functions.
2539
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2540
     * @return string The processed input value
2541
     */
2542
    public function stdWrap_cacheStore($content = '', $conf = [])
2543
    {
2544
        if (!isset($conf['cache.'])) {
2545
            return $content;
2546
        }
2547
        $key = $this->calculateCacheKey($conf['cache.']);
2548
        if (empty($key)) {
2549
            return $content;
2550
        }
2551
        /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
2552
        $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
2553
        $tags = $this->calculateCacheTags($conf['cache.']);
2554
        $lifetime = $this->calculateCacheLifetime($conf['cache.']);
2555
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) {
2556
            $params = [
2557
                'key' => $key,
2558
                'content' => $content,
2559
                'lifetime' => $lifetime,
2560
                'tags' => $tags
2561
            ];
2562
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2563
            GeneralUtility::callUserFunction($_funcRef, $params, $ref);
2564
        }
2565
        $cacheFrontend->set($key, $content, $tags, $lifetime);
2566
        return $content;
2567
    }
2568
2569
    /**
2570
     * stdWrap post process hook
2571
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2572
     * this hook executes functions at after the content has been modified by the rest of the stdWrap functions but still before debugging
2573
     *
2574
     * @param string $content Input value undergoing processing in these functions.
2575
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2576
     * @return string The processed input value
2577
     */
2578
    public function stdWrap_stdWrapPostProcess($content = '', $conf = [])
2579
    {
2580
        foreach ($this->stdWrapHookObjects as $hookObject) {
2581
            /** @var ContentObjectStdWrapHookInterface $hookObject */
2582
            $content = $hookObject->stdWrapPostProcess($content, $conf, $this);
2583
        }
2584
        return $content;
2585
    }
2586
2587
    /**
2588
     * debug
2589
     * Will output the content as readable HTML code
2590
     *
2591
     * @param string $content Input value undergoing processing in this function.
2592
     * @return string The processed input value
2593
     */
2594
    public function stdWrap_debug($content = '')
2595
    {
2596
        return '<pre>' . htmlspecialchars($content) . '</pre>';
2597
    }
2598
2599
    /**
2600
     * debugFunc
2601
     * Will output the content in a debug table
2602
     *
2603
     * @param string $content Input value undergoing processing in this function.
2604
     * @param array $conf stdWrap properties for debugFunc.
2605
     * @return string The processed input value
2606
     */
2607
    public function stdWrap_debugFunc($content = '', $conf = [])
2608
    {
2609
        debug((int)$conf['debugFunc'] === 2 ? [$content] : $content);
2610
        return $content;
2611
    }
2612
2613
    /**
2614
     * debugData
2615
     * Will output the data used by the current record in a debug table
2616
     *
2617
     * @param string $content Input value undergoing processing in this function.
2618
     * @return string The processed input value
2619
     */
2620
    public function stdWrap_debugData($content = '')
2621
    {
2622
        debug($this->data, '$cObj->data:');
2623
        if (is_array($this->alternativeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->alternativeData) is always false.
Loading history...
2624
            debug($this->alternativeData, '$this->alternativeData');
2625
        }
2626
        return $content;
2627
    }
2628
2629
    /**
2630
     * Returns number of rows selected by the query made by the properties set.
2631
     * Implements the stdWrap "numRows" property
2632
     *
2633
     * @param array $conf TypoScript properties for the property (see link to "numRows")
2634
     * @return int The number of rows found by the select
2635
     * @internal
2636
     * @see stdWrap()
2637
     */
2638
    public function numRows($conf)
2639
    {
2640
        $conf['select.']['selectFields'] = 'count(*)';
2641
        $statement = $this->exec_getQuery($conf['table'], $conf['select.']);
2642
2643
        return (int)$statement->fetchColumn(0);
2644
    }
2645
2646
    /**
2647
     * Exploding a string by the $char value (if integer its an ASCII value) and returning index $listNum
2648
     *
2649
     * @param string $content String to explode
2650
     * @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())
2651
     * @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.
2652
     * @return string
2653
     */
2654
    public function listNum($content, $listNum, $char)
2655
    {
2656
        $char = $char ?: ',';
2657
        if (MathUtility::canBeInterpretedAsInteger($char)) {
2658
            $char = chr((int)$char);
2659
        }
2660
        $temp = explode($char, $content);
2661
        if (empty($temp)) {
2662
            return '';
2663
        }
2664
        $last = '' . (count($temp) - 1);
2665
        // Take a random item if requested
2666
        if ($listNum === 'rand') {
2667
            $listNum = (string)random_int(0, count($temp) - 1);
2668
        }
2669
        $index = $this->calc(str_ireplace('last', $last, $listNum));
2670
        return $temp[$index];
2671
    }
2672
2673
    /**
2674
     * Compares values together based on the settings in the input TypoScript array and returns the comparison result.
2675
     * Implements the "if" function in TYPO3 TypoScript
2676
     *
2677
     * @param array $conf TypoScript properties defining what to compare
2678
     * @return bool
2679
     * @see stdWrap()
2680
     * @see _parseFunc()
2681
     */
2682
    public function checkIf($conf)
2683
    {
2684
        if (!is_array($conf)) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
2685
            return true;
2686
        }
2687
        if (isset($conf['directReturn'])) {
2688
            return (bool)$conf['directReturn'];
2689
        }
2690
        $flag = true;
2691
        if (isset($conf['isNull.'])) {
2692
            $isNull = $this->stdWrap('', $conf['isNull.']);
2693
            if ($isNull !== null) {
0 ignored issues
show
introduced by
The condition $isNull !== null is always true.
Loading history...
2694
                $flag = false;
2695
            }
2696
        }
2697
        if (isset($conf['isTrue']) || isset($conf['isTrue.'])) {
2698
            $isTrue = trim((string)$this->stdWrapValue('isTrue', $conf ?? []));
2699
            if (!$isTrue) {
2700
                $flag = false;
2701
            }
2702
        }
2703
        if (isset($conf['isFalse']) || isset($conf['isFalse.'])) {
2704
            $isFalse = trim((string)$this->stdWrapValue('isFalse', $conf ?? []));
2705
            if ($isFalse) {
2706
                $flag = false;
2707
            }
2708
        }
2709
        if (isset($conf['isPositive']) || isset($conf['isPositive.'])) {
2710
            $number = $this->calc((string)$this->stdWrapValue('isPositive', $conf ?? []));
2711
            if ($number < 1) {
2712
                $flag = false;
2713
            }
2714
        }
2715
        if ($flag) {
2716
            $value = trim((string)$this->stdWrapValue('value', $conf ?? []));
2717
            if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) {
2718
                $number = trim((string)$this->stdWrapValue('isGreaterThan', $conf ?? []));
2719
                if ($number <= $value) {
2720
                    $flag = false;
2721
                }
2722
            }
2723
            if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) {
2724
                $number = trim((string)$this->stdWrapValue('isLessThan', $conf ?? []));
2725
                if ($number >= $value) {
2726
                    $flag = false;
2727
                }
2728
            }
2729
            if (isset($conf['equals']) || isset($conf['equals.'])) {
2730
                $number = trim((string)$this->stdWrapValue('equals', $conf ?? []));
2731
                if ($number != $value) {
2732
                    $flag = false;
2733
                }
2734
            }
2735
            if (isset($conf['isInList']) || isset($conf['isInList.'])) {
2736
                $number = trim((string)$this->stdWrapValue('isInList', $conf ?? []));
2737
                if (!GeneralUtility::inList($value, $number)) {
2738
                    $flag = false;
2739
                }
2740
            }
2741
            if (isset($conf['bitAnd']) || isset($conf['bitAnd.'])) {
2742
                $number = (int)trim((string)$this->stdWrapValue('bitAnd', $conf ?? []));
2743
                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

2743
                if ((new BitSet($number))->get(/** @scrutinizer ignore-type */ $value) === false) {
Loading history...
2744
                    $flag = false;
2745
                }
2746
            }
2747
        }
2748
        if ($conf['negate'] ?? false) {
2749
            $flag = !$flag;
2750
        }
2751
        return $flag;
2752
    }
2753
2754
    /**
2755
     * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser"
2756
     * together with the TypoScript options which are first converted from a TS style array
2757
     * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class.
2758
     *
2759
     * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser
2760
     * @param array $conf TypoScript properties for the parser. See link.
2761
     * @return string Return value.
2762
     * @see stdWrap()
2763
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLparserConfig()
2764
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLcleaner()
2765
     */
2766
    public function HTMLparser_TSbridge($theValue, $conf)
2767
    {
2768
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
2769
        $htmlParserCfg = $htmlParser->HTMLparserConfig($conf);
2770
        return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]);
2771
    }
2772
2773
    /**
2774
     * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes.
2775
     *
2776
     * @param string $content Input string being wrapped
2777
     * @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.
2778
     * @return string Output string wrapped in the wrapping value.
2779
     * @see insertData()
2780
     * @see stdWrap()
2781
     */
2782
    public function dataWrap($content, $wrap)
2783
    {
2784
        return $this->wrap($content, $this->insertData($wrap));
2785
    }
2786
2787
    /**
2788
     * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they
2789
     * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces.
2790
     * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine
2791
     * DBAL and is skipped here for later processing.
2792
     *
2793
     * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with
2794
     * the current pages title field value.
2795
     *
2796
     * @param string $str Input value
2797
     * @return string Processed input value
2798
     * @see getData()
2799
     * @see stdWrap()
2800
     * @see dataWrap()
2801
     */
2802
    public function insertData($str)
2803
    {
2804
        $inside = 0;
2805
        $newVal = '';
2806
        $pointer = 0;
2807
        $totalLen = strlen($str);
2808
        do {
2809
            if (!$inside) {
2810
                $len = strcspn(substr($str, $pointer), '{');
2811
                $newVal .= substr($str, $pointer, $len);
2812
                $inside = true;
2813
                if (substr($str, $pointer + $len + 1, 1) === '#') {
2814
                    $len2 = strcspn(substr($str, $pointer + $len), '}');
2815
                    $newVal .= substr($str, $pointer + $len, $len2);
2816
                    $len += $len2;
2817
                    $inside = false;
2818
                }
2819
            } else {
2820
                $len = strcspn(substr($str, $pointer), '}') + 1;
2821
                $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data);
2822
                $inside = false;
2823
            }
2824
            $pointer += $len;
2825
        } while ($pointer < $totalLen);
2826
        return $newVal;
2827
    }
2828
2829
    /**
2830
     * 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.
2831
     * Notice; this function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript.
2832
     *
2833
     * @param string $str Input value
2834
     * @param array $conf TypoScript Configuration (not used at this point.)
2835
     * @param string $content The content to wrap the comment around.
2836
     * @return string Processed input value
2837
     * @see stdWrap()
2838
     */
2839
    public function prefixComment($str, $conf, $content)
2840
    {
2841
        if (empty($str)) {
2842
            return $content;
2843
        }
2844
        $parts = explode('|', $str);
2845
        $indent = (int)$parts[0];
2846
        $comment = htmlspecialchars($this->insertData($parts[1]));
2847
        $output = LF
2848
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF
2849
            . str_pad('', $indent + 1, "\t") . $content . LF
2850
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF
2851
            . str_pad('', $indent + 1, "\t");
2852
        return $output;
2853
    }
2854
2855
    /**
2856
     * Implements the stdWrap property "substring" which is basically a TypoScript implementation of the PHP function, substr()
2857
     *
2858
     * @param string $content The string to perform the operation on
2859
     * @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().
2860
     * @return string The processed input value.
2861
     * @internal
2862
     * @see stdWrap()
2863
     */
2864
    public function substring($content, $options)
2865
    {
2866
        $options = GeneralUtility::intExplode(',', $options . ',');
2867
        if ($options[1]) {
2868
            return mb_substr($content, $options[0], $options[1], 'utf-8');
2869
        }
2870
        return mb_substr($content, $options[0], null, 'utf-8');
2871
    }
2872
2873
    /**
2874
     * 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.
2875
     *
2876
     * @param string $content The string to perform the operation on
2877
     * @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.
2878
     * @return string The processed input value.
2879
     * @internal
2880
     * @see stdWrap()
2881
     */
2882
    public function crop($content, $options)
2883
    {
2884
        $options = explode('|', $options);
2885
        $chars = (int)$options[0];
2886
        $afterstring = trim($options[1] ?? '');
2887
        $crop2space = trim($options[2] ?? '');
2888
        if ($chars) {
2889
            if (mb_strlen($content, 'utf-8') > abs($chars)) {
2890
                $truncatePosition = false;
2891
                if ($chars < 0) {
2892
                    $content = mb_substr($content, $chars, null, 'utf-8');
2893
                    if ($crop2space) {
2894
                        $truncatePosition = strpos($content, ' ');
2895
                    }
2896
                    $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content;
2897
                } else {
2898
                    $content = mb_substr($content, 0, $chars, 'utf-8');
2899
                    if ($crop2space) {
2900
                        $truncatePosition = strrpos($content, ' ');
2901
                    }
2902
                    $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring;
2903
                }
2904
            }
2905
        }
2906
        return $content;
2907
    }
2908
2909
    /**
2910
     * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length
2911
     * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string
2912
     * really was cropped.
2913
     *
2914
     * Compared to stdWrap.crop it respects HTML tags and entities.
2915
     *
2916
     * @param string $content The string to perform the operation on
2917
     * @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.
2918
     * @return string The processed input value.
2919
     * @internal
2920
     * @see stdWrap()
2921
     */
2922
    public function cropHTML($content, $options)
2923
    {
2924
        $options = explode('|', $options);
2925
        $chars = (int)$options[0];
2926
        $absChars = abs($chars);
2927
        $replacementForEllipsis = trim($options[1] ?? '');
2928
        $crop2space = trim($options[2] ?? '') === '1';
2929
        // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
2930
        $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';
2931
        $tagsRegEx = '
2932
			(
2933
				(?:
2934
					<!--.*?-->					# a comment
2935
					|
2936
					<canvas[^>]*>.*?</canvas>   # a canvas tag
2937
					|
2938
					<script[^>]*>.*?</script>   # a script tag
2939
					|
2940
					<noscript[^>]*>.*?</noscript> # a noscript tag
2941
					|
2942
					<template[^>]*>.*?</template> # a template tag
2943
				)
2944
				|
2945
				</?(?:' . $tags . ')+			# opening tag (\'<tag\') or closing tag (\'</tag\')
2946
				(?:
2947
					(?:
2948
						(?:
2949
							\\s+\\w[\\w-]*		# EITHER spaces, followed by attribute names
2950
							(?:
2951
								\\s*=?\\s*		# equals
2952
								(?>
2953
									".*?"		# attribute values in double-quotes
2954
									|
2955
									\'.*?\'		# attribute values in single-quotes
2956
									|
2957
									[^\'">\\s]+	# plain attribute values
2958
								)
2959
							)?
2960
						)
2961
						|						# OR a single dash (for TYPO3 link tag)
2962
						(?:
2963
							\\s+-
2964
						)
2965
					)+\\s*
2966
					|							# OR only spaces
2967
					\\s*
2968
				)
2969
				/?>								# closing the tag with \'>\' or \'/>\'
2970
			)';
2971
        $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
2972
        if ($splittedContent === false) {
2973
            $this->logger->debug('Unable to split "' . $content . '" into tags.');
2974
            $splittedContent = [];
2975
        }
2976
        // Reverse array if we are cropping from right.
2977
        if ($chars < 0) {
2978
            $splittedContent = array_reverse($splittedContent);
2979
        }
2980
        // Crop the text (chars of tag-blocks are not counted).
2981
        $strLen = 0;
2982
        // This is the offset of the content item which was cropped.
2983
        $croppedOffset = null;
2984
        $countSplittedContent = count($splittedContent);
2985
        for ($offset = 0; $offset < $countSplittedContent; $offset++) {
2986
            if ($offset % 2 === 0) {
2987
                $tempContent = $splittedContent[$offset];
2988
                $thisStrLen = mb_strlen(html_entity_decode($tempContent, ENT_COMPAT, 'UTF-8'), 'utf-8');
2989
                if ($strLen + $thisStrLen > $absChars) {
2990
                    $croppedOffset = $offset;
2991
                    $cropPosition = $absChars - $strLen;
2992
                    // The snippet "&[^&\s;]{2,8};" in the RegEx below represents entities.
2993
                    $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)';
2994
                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}#uis';
2995
                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
2996
                        $tempContentPlusOneCharacter = $croppedMatch[0];
2997
                    } else {
2998
                        $tempContentPlusOneCharacter = false;
2999
                    }
3000
                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}#uis';
3001
                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3002
                        $tempContent = $croppedMatch[0];
3003
                        if ($crop2space && $tempContentPlusOneCharacter !== false) {
3004
                            $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}(?=\\s)#uis';
3005
                            if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) {
3006
                                $tempContent = $croppedMatch[0];
3007
                            }
3008
                        }
3009
                    }
3010
                    $splittedContent[$offset] = $tempContent;
3011
                    break;
3012
                }
3013
                $strLen += $thisStrLen;
3014
            }
3015
        }
3016
        // Close cropped tags.
3017
        $closingTags = [];
3018
        if ($croppedOffset !== null) {
3019
            $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
3020
            $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
3021
            for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
3022
                if (substr($splittedContent[$offset], -2) === '/>') {
3023
                    // Ignore empty element tags (e.g. <br />).
3024
                    continue;
3025
                }
3026
                preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches);
3027
                $tagName = $matches[1] ?? null;
3028
                if ($tagName !== null) {
3029
                    // Seek for the closing (or opening) tag.
3030
                    $countSplittedContent = count($splittedContent);
3031
                    for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) {
3032
                        preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches);
3033
                        $seekingTagName = $matches[1] ?? null;
3034
                        if ($tagName === $seekingTagName) {
3035
                            // We found a matching tag.
3036
                            // Add closing tag only if it occurs after the cropped content item.
3037
                            if ($seekingOffset > $croppedOffset) {
3038
                                $closingTags[] = $splittedContent[$seekingOffset];
3039
                            }
3040
                            break;
3041
                        }
3042
                    }
3043
                }
3044
            }
3045
            // Drop the cropped items of the content array. The $closingTags will be added later on again.
3046
            array_splice($splittedContent, $croppedOffset + 1);
3047
        }
3048
        $splittedContent = array_merge($splittedContent, [
3049
            $croppedOffset !== null ? $replacementForEllipsis : ''
3050
        ], $closingTags);
3051
        // Reverse array once again if we are cropping from the end.
3052
        if ($chars < 0) {
3053
            $splittedContent = array_reverse($splittedContent);
3054
        }
3055
        return implode('', $splittedContent);
3056
    }
3057
3058
    /**
3059
     * 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())
3060
     *
3061
     * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
3062
     * @return int The result (might be a float if you did a division of the numbers).
3063
     * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()
3064
     */
3065
    public function calc($val)
3066
    {
3067
        $parts = GeneralUtility::splitCalc($val, '+-*/');
3068
        $value = 0;
3069
        foreach ($parts as $part) {
3070
            $theVal = $part[1];
3071
            $sign = $part[0];
3072
            if ((string)(int)$theVal === (string)$theVal) {
3073
                $theVal = (int)$theVal;
3074
            } else {
3075
                $theVal = 0;
3076
            }
3077
            if ($sign === '-') {
3078
                $value -= $theVal;
3079
            }
3080
            if ($sign === '+') {
3081
                $value += $theVal;
3082
            }
3083
            if ($sign === '/') {
3084
                if ((int)$theVal) {
3085
                    $value /= (int)$theVal;
3086
                }
3087
            }
3088
            if ($sign === '*') {
3089
                $value *= $theVal;
3090
            }
3091
        }
3092
        return $value;
3093
    }
3094
3095
    /**
3096
     * 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.
3097
     * 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.
3098
     * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse.
3099
     *
3100
     * @param string $value The string value to explode by $conf[token] and process each part
3101
     * @param array $conf TypoScript properties for "split
3102
     * @return string Compiled result
3103
     * @internal
3104
     * @see stdWrap()
3105
     * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::processItemStates()
3106
     */
3107
    public function splitObj($value, $conf)
3108
    {
3109
        $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'], $conf['token.']) : $conf['token'];
3110
        if ($conf['token'] === '') {
3111
            return $value;
3112
        }
3113
        $valArr = explode($conf['token'], $value);
3114
3115
        // return value directly by returnKey. No further processing
3116
        if (!empty($valArr) && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))) {
3117
            $key = (int)$this->stdWrapValue('returnKey', $conf ?? []);
3118
            return $valArr[$key] ?? '';
3119
        }
3120
3121
        // return the amount of elements. No further processing
3122
        if (!empty($valArr) && ($conf['returnCount'] || $conf['returnCount.'])) {
3123
            $returnCount = (bool)$this->stdWrapValue('returnCount', $conf ?? []);
3124
            return $returnCount ? count($valArr) : 0;
3125
        }
3126
3127
        // calculate splitCount
3128
        $splitCount = count($valArr);
3129
        $max = (int)$this->stdWrapValue('max', $conf ?? []);
3130
        if ($max && $splitCount > $max) {
3131
            $splitCount = $max;
3132
        }
3133
        $min = (int)$this->stdWrapValue('min', $conf ?? []);
3134
        if ($min && $splitCount < $min) {
3135
            $splitCount = $min;
3136
        }
3137
        $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3138
        $cObjNumSplitConf = isset($conf['cObjNum.']) ? (string)$this->stdWrap($conf['cObjNum'], $conf['cObjNum.']) : (string)$conf['cObjNum'];
3139
        $splitArr = [];
3140
        if ($wrap !== '' || $cObjNumSplitConf !== '') {
3141
            $splitArr['wrap'] = $wrap;
3142
            $splitArr['cObjNum'] = $cObjNumSplitConf;
3143
            $splitArr = GeneralUtility::makeInstance(TypoScriptService::class)
3144
                ->explodeConfigurationForOptionSplit($splitArr, $splitCount);
3145
        }
3146
        $content = '';
3147
        for ($a = 0; $a < $splitCount; $a++) {
3148
            $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a;
3149
            $value = '' . $valArr[$a];
3150
            $this->data[$this->currentValKey] = $value;
3151
            if ($splitArr[$a]['cObjNum']) {
3152
                $objName = (int)$splitArr[$a]['cObjNum'];
3153
                $value = isset($conf[$objName . '.'])
3154
                    ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.'])
3155
                    : $this->cObjGet($conf[$objName . '.'], $objName . '.');
3156
            }
3157
            $wrap = (string)$this->stdWrapValue('wrap', $splitArr[$a]);
3158
            if ($wrap) {
3159
                $value = $this->wrap($value, $wrap);
3160
            }
3161
            $content .= $value;
3162
        }
3163
        return $content;
3164
    }
3165
3166
    /**
3167
     * Processes ordered replacements on content data.
3168
     *
3169
     * @param string $content The content to be processed
3170
     * @param array $configuration The TypoScript configuration for stdWrap.replacement
3171
     * @return string The processed content data
3172
     */
3173
    protected function replacement($content, array $configuration)
3174
    {
3175
        // Sorts actions in configuration by numeric index
3176
        ksort($configuration, SORT_NUMERIC);
3177
        foreach ($configuration as $index => $action) {
3178
            // Checks whether we have a valid action and a numeric key ending with a dot ("10.")
3179
            if (is_array($action) && substr($index, -1) === '.' && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) {
3180
                $content = $this->replacementSingle($content, $action);
3181
            }
3182
        }
3183
        return $content;
3184
    }
3185
3186
    /**
3187
     * Processes a single search/replace on content data.
3188
     *
3189
     * @param string $content The content to be processed
3190
     * @param array $configuration The TypoScript of the search/replace action to be processed
3191
     * @return string The processed content data
3192
     */
3193
    protected function replacementSingle($content, array $configuration)
3194
    {
3195
        if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) {
3196
            // Gets the strings
3197
            $search = (string)$this->stdWrapValue('search', $configuration ?? []);
3198
            $replace = (string)$this->stdWrapValue('replace', $configuration, null);
3199
3200
            // Determines whether regular expression shall be used
3201
            $useRegularExpression = (bool)$this->stdWrapValue('useRegExp', $configuration, false);
3202
3203
            // Determines whether replace-pattern uses option-split
3204
            $useOptionSplitReplace = (bool)$this->stdWrapValue('useOptionSplitReplace', $configuration, false);
3205
3206
            // Performs a replacement by preg_replace()
3207
            if ($useRegularExpression) {
3208
                // Get separator-character which precedes the string and separates search-string from the modifiers
3209
                $separator = $search[0];
3210
                $startModifiers = strrpos($search, $separator);
3211
                if ($separator !== false && $startModifiers > 0) {
3212
                    $modifiers = substr($search, $startModifiers + 1);
3213
                    // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code
3214
                    $modifiers = str_replace('e', '', $modifiers);
3215
                    $search = substr($search, 0, $startModifiers + 1) . $modifiers;
3216
                }
3217
                if ($useOptionSplitReplace) {
3218
                    // init for replacement
3219
                    $splitCount = preg_match_all($search, $content, $matches);
3220
                    $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3221
                    $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3222
                    $replaceCount = 0;
3223
3224
                    $replaceCallback = function ($match) use ($replaceArray, $search, &$replaceCount) {
3225
                        $replaceCount++;
3226
                        return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]);
3227
                    };
3228
                    $content = preg_replace_callback($search, $replaceCallback, $content);
3229
                } else {
3230
                    $content = preg_replace($search, $replace, $content);
3231
                }
3232
            } elseif ($useOptionSplitReplace) {
3233
                // turn search-string into a preg-pattern
3234
                $searchPreg = '#' . preg_quote($search, '#') . '#';
3235
3236
                // init for replacement
3237
                $splitCount = preg_match_all($searchPreg, $content, $matches);
3238
                $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3239
                $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3240
                $replaceCount = 0;
3241
3242
                $replaceCallback = function () use ($replaceArray, &$replaceCount) {
3243
                    $replaceCount++;
3244
                    return $replaceArray[$replaceCount - 1][0];
3245
                };
3246
                $content = preg_replace_callback($searchPreg, $replaceCallback, $content);
3247
            } else {
3248
                $content = str_replace($search, $replace, $content);
3249
            }
3250
        }
3251
        return $content;
3252
    }
3253
3254
    /**
3255
     * Implements the "round" property of stdWrap
3256
     * This is a Wrapper function for PHP's rounding functions (round,ceil,floor), defaults to round()
3257
     *
3258
     * @param string $content Value to process
3259
     * @param array $conf TypoScript configuration for round
3260
     * @return string The formatted number
3261
     */
3262
    protected function round($content, array $conf = [])
3263
    {
3264
        $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3265
        $type = $this->stdWrapValue('roundType', $conf ?? []);
3266
        $floatVal = (float)$content;
3267
        switch ($type) {
3268
            case 'ceil':
3269
                $content = ceil($floatVal);
3270
                break;
3271
            case 'floor':
3272
                $content = floor($floatVal);
3273
                break;
3274
            case 'round':
3275
3276
            default:
3277
                $content = round($floatVal, $decimals);
3278
        }
3279
        return $content;
3280
    }
3281
3282
    /**
3283
     * Implements the stdWrap property "numberFormat"
3284
     * This is a Wrapper function for php's number_format()
3285
     *
3286
     * @param float $content Value to process
3287
     * @param array $conf TypoScript Configuration for numberFormat
3288
     * @return string The formatted number
3289
     */
3290
    public function numberFormat($content, $conf)
3291
    {
3292
        $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3293
        $dec_point = (string)$this->stdWrapValue('dec_point', $conf, '.');
3294
        $thousands_sep = (string)$this->stdWrapValue('thousands_sep', $conf, ',');
3295
        return number_format((float)$content, $decimals, $dec_point, $thousands_sep);
3296
    }
3297
3298
    /**
3299
     * Implements the stdWrap property, "parseFunc".
3300
     * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text
3301
     * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc.
3302
     * It is still a very important function for processing of bodytext which is normally stored in the database
3303
     * in a format which is not fully ready to be outputted.
3304
     * This situation has not become better by having a RTE around...
3305
     *
3306
     * This function is actually just splitting the input content according to the configuration of "external blocks".
3307
     * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed
3308
     * (while other parts/blocks should NOT be parsed).
3309
     * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc()
3310
     *
3311
     * @param string $theValue The value to process.
3312
     * @param array $conf TypoScript configuration for parseFunc
3313
     * @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!
3314
     * @return string The processed value
3315
     * @see _parseFunc()
3316
     */
3317
    public function parseFunc($theValue, $conf, $ref = '')
3318
    {
3319
        // Fetch / merge reference, if any
3320
        if ($ref) {
3321
            $temp_conf = [
3322
                'parseFunc' => $ref,
3323
                'parseFunc.' => $conf
3324
            ];
3325
            $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc');
3326
            $conf = $temp_conf['parseFunc.'];
3327
        }
3328
        // Process:
3329
        if ((string)($conf['externalBlocks'] ?? '') === '') {
3330
            return $this->_parseFunc($theValue, $conf);
3331
        }
3332
        $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks'])));
3333
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3334
        $parts = $htmlParser->splitIntoBlock($tags, $theValue);
3335
        foreach ($parts as $k => $v) {
3336
            if ($k % 2) {
3337
                // font:
3338
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3339
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3340
                if ($cfg['stripNLprev'] || $cfg['stripNL']) {
3341
                    $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]);
3342
                }
3343
                if ($cfg['stripNLnext'] || $cfg['stripNL']) {
3344
                    $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]);
3345
                }
3346
            }
3347
        }
3348
        foreach ($parts as $k => $v) {
3349
            if ($k % 2) {
3350
                $tag = $htmlParser->getFirstTag($v);
3351
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3352
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3353
                if ($cfg['callRecursive']) {
3354
                    $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf);
3355
                    if (!$cfg['callRecursive.']['dontWrapSelf']) {
3356
                        if ($cfg['callRecursive.']['alternativeWrap']) {
3357
                            $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']);
3358
                        } else {
3359
                            if (is_array($cfg['callRecursive.']['tagStdWrap.'])) {
3360
                                $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']);
3361
                            }
3362
                            $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>';
3363
                        }
3364
                    }
3365
                } elseif ($cfg['HTMLtableCells']) {
3366
                    $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]);
3367
                    foreach ($rowParts as $kk => $vv) {
3368
                        if ($kk % 2) {
3369
                            $colParts = $htmlParser->splitIntoBlock('td,th', $vv);
3370
                            $cc = 0;
3371
                            foreach ($colParts as $kkk => $vvv) {
3372
                                if ($kkk % 2) {
3373
                                    $cc++;
3374
                                    $tag = $htmlParser->getFirstTag($vvv);
3375
                                    $tagName = strtolower($htmlParser->getFirstTagName($vvv));
3376
                                    $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv);
3377
                                    if ($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] || !isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && $cfg['HTMLtableCells.']['default.']['callRecursive']) {
3378
                                        if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs']) {
3379
                                            $colParts[$kkk] = str_replace('</p><p>', '</p>' . LF . '<p>', $colParts[$kkk]);
3380
                                        }
3381
                                        $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf);
3382
                                    }
3383
                                    $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'])
3384
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']
3385
                                        : $cfg['HTMLtableCells.']['default.']['tagStdWrap.'];
3386
                                    if (is_array($tagStdWrap)) {
3387
                                        $tag = $this->stdWrap($tag, $tagStdWrap);
3388
                                    }
3389
                                    $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'])
3390
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']
3391
                                        : $cfg['HTMLtableCells.']['default.']['stdWrap.'];
3392
                                    if (is_array($stdWrap)) {
3393
                                        $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap);
3394
                                    }
3395
                                    $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>';
3396
                                }
3397
                            }
3398
                            $rowParts[$kk] = implode('', $colParts);
3399
                        }
3400
                    }
3401
                    $parts[$k] = implode('', $rowParts);
3402
                }
3403
                if (is_array($cfg['stdWrap.'])) {
3404
                    $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
3405
                }
3406
            } else {
3407
                $parts[$k] = $this->_parseFunc($parts[$k], $conf);
3408
            }
3409
        }
3410
        return implode('', $parts);
3411
    }
3412
3413
    /**
3414
     * Helper function for parseFunc()
3415
     *
3416
     * @param string $theValue The value to process.
3417
     * @param array $conf TypoScript configuration for parseFunc
3418
     * @return string The processed value
3419
     * @internal
3420
     * @see parseFunc()
3421
     */
3422
    public function _parseFunc($theValue, $conf)
3423
    {
3424
        if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
3425
            return $theValue;
3426
        }
3427
        // Indicates that the data is from within a tag.
3428
        $inside = false;
3429
        // Pointer to the total string position
3430
        $pointer = 0;
3431
        // Loaded with the current typo-tag if any.
3432
        $currentTag = null;
3433
        $stripNL = 0;
3434
        $contentAccum = [];
3435
        $contentAccumP = 0;
3436
        $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? ''));
3437
        $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? ''));
3438
        $totalLen = strlen($theValue);
3439
        do {
3440
            if (!$inside) {
3441
                if ($currentTag === null) {
3442
                    // These operations should only be performed on code outside the typotags...
3443
                    // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/'
3444
                    $len_p = 0;
3445
                    $c = 100;
3446
                    do {
3447
                        $len = strcspn(substr($theValue, $pointer + $len_p), '<');
3448
                        $len_p += $len + 1;
3449
                        $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1)));
3450
                        $c--;
3451
                    } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47);
3452
                    $len = $len_p - 1;
3453
                } else {
3454
                    $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, (string)$currentTag[0]);
3455
                }
3456
                // $data is the content until the next <tag-start or end is detected.
3457
                // In case of a currentTag set, this would mean all data between the start- and end-tags
3458
                $data = substr($theValue, $pointer, $len);
3459
                if ($data !== false) {
3460
                    if ($stripNL) {
3461
                        // If the previous tag was set to strip NewLines in the beginning of the next data-chunk.
3462
                        $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data);
3463
                        if ($data === null) {
3464
                            $this->logger->debug('Stripping new lines failed for "' . $data . '"');
0 ignored issues
show
Bug introduced by
Are you sure $data of type void 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

3464
                            $this->logger->debug('Stripping new lines failed for "' . /** @scrutinizer ignore-type */ $data . '"');
Loading history...
3465
                            $data = '';
3466
                        }
3467
                    }
3468
                    // These operations should only be performed on code outside the tags...
3469
                    if (!is_array($currentTag)) {
3470
                        // Constants
3471
                        $tsfe = $this->getTypoScriptFrontendController();
3472
                        $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null;
3473
                        if (!empty($conf['constants']) && is_array($tmpConstants)) {
3474
                            foreach ($tmpConstants as $key => $val) {
3475
                                if (is_string($val)) {
3476
                                    $data = str_replace('###' . $key . '###', $val, $data);
3477
                                }
3478
                            }
3479
                        }
3480
                        // Short
3481
                        if (isset($conf['short.']) && is_array($conf['short.'])) {
3482
                            $shortWords = $conf['short.'];
3483
                            krsort($shortWords);
3484
                            foreach ($shortWords as $key => $val) {
3485
                                if (is_string($val)) {
3486
                                    $data = str_replace($key, $val, $data);
3487
                                }
3488
                            }
3489
                        }
3490
                        // stdWrap
3491
                        if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) {
3492
                            $data = $this->stdWrap($data, $conf['plainTextStdWrap.']);
3493
                        }
3494
                        // userFunc
3495
                        if ($conf['userFunc'] ?? false) {
3496
                            $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $data);
3497
                        }
3498
                        // Makelinks: (Before search-words as we need the links to be generated when searchwords go on...!)
3499
                        if ($conf['makelinks'] ?? false) {
3500
                            $data = $this->http_makelinks($data, $conf['makelinks.']['http.']);
3501
                            $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []);
3502
                        }
3503
                        // Search Words:
3504
                        if ($tsfe->no_cache && $conf['sword'] && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) {
3505
                            $newstring = '';
3506
                            do {
3507
                                $pregSplitMode = 'i';
3508
                                if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) {
3509
                                    $pregSplitMode = '';
3510
                                }
3511
                                $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2);
3512
                                $newstring .= $pieces[0];
3513
                                $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1]));
3514
                                $inTag = false;
3515
                                if (strpos($pieces[0], '<') !== false || strpos($pieces[0], '>') !== false) {
3516
                                    // Returns TRUE, if a '<' is closer to the string-end than '>'.
3517
                                    // This is the case if we're INSIDE a tag (that could have been
3518
                                    // made by makelinks...) and we must secure, that the inside of a tag is
3519
                                    // not marked up.
3520
                                    $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>');
3521
                                }
3522
                                // The searchword:
3523
                                $match = substr($data, strlen($pieces[0]), $match_len);
3524
                                if (trim($match) && strlen($match) > 1 && !$inTag) {
3525
                                    $match = $this->wrap($match, $conf['sword']);
3526
                                }
3527
                                // Concatenate the Search Word again.
3528
                                $newstring .= $match;
3529
                                $data = $pieces[1];
3530
                            } while ($pieces[1]);
3531
                            $data = $newstring;
3532
                        }
3533
                    }
3534
                    // Search for tags to process in current data and
3535
                    // call this method recursively if found
3536
                    if (strpos($data, '<') !== false && isset($conf['tags.']) && is_array($conf['tags.'])) {
3537
                        foreach ($conf['tags.'] as $tag => $tagConfig) {
3538
                            // only match tag `a` in `<a href"...">` but not in `<abbr>`
3539
                            if (preg_match('#<' . $tag . '[\s/>]#', $data)) {
3540
                                $data = $this->_parseFunc($data, $conf);
3541
                                break;
3542
                            }
3543
                        }
3544
                    }
3545
                    $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3546
                        ? $contentAccum[$contentAccumP] . $data
3547
                        : $data;
3548
                }
3549
                $inside = true;
3550
            } else {
3551
                // tags
3552
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
3553
                $data = substr($theValue, $pointer, $len);
3554
                if (StringUtility::endsWith($data, '/>') && strpos($data, '<link ') !== 0) {
3555
                    $tagContent = substr($data, 1, -2);
3556
                } else {
3557
                    $tagContent = substr($data, 1, -1);
3558
                }
3559
                $tag = explode(' ', trim($tagContent), 2);
3560
                $tag[0] = strtolower($tag[0]);
3561
                // end tag like </li>
3562
                if ($tag[0][0] === '/') {
3563
                    $tag[0] = substr($tag[0], 1);
3564
                    $tag['out'] = 1;
3565
                }
3566
                if ($conf['tags.'][$tag[0]] ?? false) {
3567
                    $treated = false;
3568
                    $stripNL = false;
3569
                    // in-tag
3570
                    if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) {
3571
                        // $currentTag (array!) is the tag we are currently processing
3572
                        $currentTag = $tag;
3573
                        $contentAccumP++;
3574
                        $treated = true;
3575
                        // in-out-tag: img and other empty tags
3576
                        if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', (string)$tag[0])) {
3577
                            $tag['out'] = 1;
3578
                        }
3579
                    }
3580
                    // out-tag
3581
                    if ($currentTag[0] === $tag[0] && isset($tag['out']) && $tag['out']) {
3582
                        $theName = $conf['tags.'][$tag[0]];
3583
                        $theConf = $conf['tags.'][$tag[0] . '.'];
3584
                        // This flag indicates, that NL- (13-10-chars) should be stripped first and last.
3585
                        $stripNL = (bool)($theConf['stripNL'] ?? false);
3586
                        // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content.
3587
                        $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false);
3588
                        $this->parameters = [];
3589
                        if (isset($currentTag[1])) {
3590
                            // decode HTML entities in attributes, since they're processed
3591
                            $params = GeneralUtility::get_tag_attributes((string)$currentTag[1], true);
3592
                            if (is_array($params)) {
3593
                                foreach ($params as $option => $val) {
3594
                                    // contains non-encoded values
3595
                                    $this->parameters[strtolower($option)] = $val;
3596
                                }
3597
                            }
3598
                            $this->parameters['allParams'] = trim((string)$currentTag[1]);
3599
                        }
3600
                        // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer.
3601
                        // $stripNL depends on the configuration of the current tag
3602
                        if ($stripNL) {
3603
                            $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1]);
3604
                            $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP]);
3605
                            $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP]);
3606
                        }
3607
                        $this->data[$this->currentValKey] = $contentAccum[$contentAccumP];
3608
                        $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]);
3609
                        // fetch the content object
3610
                        $contentAccum[$contentAccumP] = $newInput;
3611
                        $contentAccumP++;
3612
                        // If the TypoTag section
3613
                        if (!$breakOut) {
3614
                            if (!isset($contentAccum[$contentAccumP - 2])) {
3615
                                $contentAccum[$contentAccumP - 2] = '';
3616
                            }
3617
                            $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? '');
3618
                            unset($contentAccum[$contentAccumP]);
3619
                            unset($contentAccum[$contentAccumP - 1]);
3620
                            $contentAccumP -= 2;
3621
                        }
3622
                        $currentTag = null;
3623
                        $treated = true;
3624
                    }
3625
                    // other tags
3626
                    if (!$treated) {
3627
                        $contentAccum[$contentAccumP] .= $data;
3628
                    }
3629
                } else {
3630
                    // If a tag was not a typo tag, then it is just added to the content
3631
                    $stripNL = false;
3632
                    if (GeneralUtility::inList($allowTags, (string)$tag[0]) ||
3633
                        ($denyTags !== '*' && !GeneralUtility::inList($denyTags, (string)$tag[0]))) {
3634
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3635
                            ? $contentAccum[$contentAccumP] . $data
3636
                            : $data;
3637
                    } else {
3638
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3639
                            ? $contentAccum[$contentAccumP] . htmlspecialchars($data)
3640
                            : htmlspecialchars($data);
3641
                    }
3642
                }
3643
                $inside = false;
3644
            }
3645
            $pointer += $len;
3646
        } while ($pointer < $totalLen);
3647
        // Parsing nonTypoTag content (all even keys):
3648
        reset($contentAccum);
3649
        $contentAccumCount = count($contentAccum);
3650
        for ($a = 0; $a < $contentAccumCount; $a++) {
3651
            if ($a % 2 != 1) {
3652
                // stdWrap
3653
                if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) {
3654
                    $contentAccum[$a] = $this->stdWrap((string)($contentAccum[$a] ?? ''), $conf['nonTypoTagStdWrap.']);
3655
                }
3656
                // userFunc
3657
                if (!empty($conf['nonTypoTagUserFunc'])) {
3658
                    $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'], (string)($contentAccum[$a] ?? ''));
3659
                }
3660
            }
3661
        }
3662
        return implode('', $contentAccum);
3663
    }
3664
3665
    /**
3666
     * Lets you split the content by LF and process each line independently. Used to format content made with the RTE.
3667
     *
3668
     * @param string $theValue The input value
3669
     * @param array $conf TypoScript options
3670
     * @return string The processed input value being returned; Splitted lines imploded by LF again.
3671
     * @internal
3672
     */
3673
    public function encaps_lineSplit($theValue, $conf)
3674
    {
3675
        if ((string)$theValue === '') {
3676
            return '';
3677
        }
3678
        $lParts = explode(LF, $theValue);
3679
3680
        // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line.
3681
        $lastPartIndex = count($lParts) - 1;
3682
        if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') {
3683
            array_pop($lParts);
3684
        }
3685
3686
        $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList']), true);
3687
        $nonWrappedTag = $conf['nonWrappedTag'];
3688
        $defaultAlign = trim((string)$this->stdWrapValue('defaultAlign', $conf ?? []));
3689
3690
        $str_content = '';
3691
        foreach ($lParts as $k => $l) {
3692
            $sameBeginEnd = 0;
3693
            $emptyTag = false;
3694
            $l = trim($l);
3695
            $attrib = [];
3696
            $nonWrapped = false;
3697
            $tagName = '';
3698
            if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') {
3699
                $fwParts = explode('>', substr($l, 1), 2);
3700
                [$tagName] = explode(' ', $fwParts[0], 2);
3701
                if (!$fwParts[1]) {
3702
                    if (substr($tagName, -1) === '/') {
3703
                        $tagName = substr($tagName, 0, -1);
3704
                    }
3705
                    if (substr($fwParts[0], -1) === '/') {
3706
                        $sameBeginEnd = 1;
3707
                        $emptyTag = true;
3708
                        // decode HTML entities, they're encoded later again
3709
                        $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true);
3710
                    }
3711
                } else {
3712
                    $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2);
3713
                    // decode HTML entities, they're encoded later again
3714
                    $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true);
3715
                    $str_content = $backParts[0];
3716
                    $sameBeginEnd = substr(strtolower($backParts[1]), 1, strlen($tagName)) === strtolower($tagName);
3717
                }
3718
            }
3719
            if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) {
3720
                $uTagName = strtoupper($tagName);
3721
                $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName);
3722
            } else {
3723
                $uTagName = strtoupper($nonWrappedTag);
3724
                // The line will be wrapped: $uTagName should not be an empty tag
3725
                $emptyTag = false;
3726
                $str_content = $lParts[$k];
3727
                $nonWrapped = true;
3728
                $attrib = [];
3729
            }
3730
            // Wrapping all inner-content:
3731
            if (is_array($conf['innerStdWrap_all.'])) {
3732
                $str_content = $this->stdWrap($str_content, $conf['innerStdWrap_all.']);
3733
            }
3734
            if ($uTagName) {
3735
                // Setting common attributes
3736
                if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) {
3737
                    foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) {
3738
                        if (!is_array($vv)) {
3739
                            if ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'blank') {
3740
                                if ((string)($attrib[$kk] ?? '') === '') {
3741
                                    $attrib[$kk] = $vv;
3742
                                }
3743
                            } elseif ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'exists') {
3744
                                if (!isset($attrib[$kk])) {
3745
                                    $attrib[$kk] = $vv;
3746
                                }
3747
                            } else {
3748
                                $attrib[$kk] = $vv;
3749
                            }
3750
                        }
3751
                    }
3752
                }
3753
                // Wrapping all inner-content:
3754
                if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) {
3755
                    $str_content = $this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']);
3756
                }
3757
                // Default align
3758
                if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) {
3759
                    $attrib['align'] = $defaultAlign;
3760
                }
3761
                // implode (insecure) attributes, that's why `htmlspecialchars` is used here
3762
                $params = GeneralUtility::implodeAttributes($attrib, true);
3763
                if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) {
3764
                    $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
3765
                    if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) {
3766
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />';
3767
                    } else {
3768
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>';
3769
                    }
3770
                }
3771
            }
3772
            if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) {
3773
                $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']);
3774
            }
3775
            $lParts[$k] = $str_content;
3776
        }
3777
        return implode(LF, $lParts);
3778
    }
3779
3780
    /**
3781
     * Finds URLS in text and makes it to a real link.
3782
     * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link,
3783
     * linking to the URL we should have found.
3784
     *
3785
     * @param string $data The string in which to search for "http://
3786
     * @param array $conf Configuration for makeLinks, see link
3787
     * @return string The processed input string, being returned.
3788
     * @see _parseFunc()
3789
     */
3790
    public function http_makelinks($data, $conf)
3791
    {
3792
        $parts = [];
3793
        $aTagParams = $this->getATagParams($conf);
3794
        $textstr = '';
3795
        foreach (['http://', 'https://'] as $scheme) {
3796
            $textpieces = explode($scheme, $data);
3797
            $pieces = count($textpieces);
3798
            $textstr = $textpieces[0];
3799
            for ($i = 1; $i < $pieces; $i++) {
3800
                $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3801
                if (trim(substr($textstr, -1)) === '' && $len) {
3802
                    $lastChar = substr($textpieces[$i], $len - 1, 1);
3803
                    if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) {
3804
                        $len--;
3805
                    }
3806
                    // Included '\/' 3/12
3807
                    $parts[0] = substr($textpieces[$i], 0, $len);
3808
                    $parts[1] = substr($textpieces[$i], $len);
3809
                    $keep = $conf['keep'];
3810
                    $linkParts = parse_url($scheme . $parts[0]);
3811
                    $linktxt = '';
3812
                    if (strpos($keep, 'scheme') !== false) {
3813
                        $linktxt = $scheme;
3814
                    }
3815
                    $linktxt .= $linkParts['host'];
3816
                    if (strpos($keep, 'path') !== false) {
3817
                        $linktxt .= $linkParts['path'];
3818
                        // Added $linkParts['query'] 3/12
3819
                        if (strpos($keep, 'query') !== false && $linkParts['query']) {
3820
                            $linktxt .= '?' . $linkParts['query'];
3821
                        } elseif ($linkParts['path'] === '/') {
3822
                            $linktxt = substr($linktxt, 0, -1);
3823
                        }
3824
                    }
3825
                    $target = (string)$this->stdWrapValue('extTarget', $conf, $this->getTypoScriptFrontendController()->extTarget);
3826
3827
                    // check for jump URLs or similar
3828
                    $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf) ?? '';
3829
3830
                    $res = '<a href="' . htmlspecialchars($linkUrl) . '"'
3831
                        . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
3832
                        . $aTagParams . '>';
3833
3834
                    $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3835
                    if ((string)$conf['ATagBeforeWrap'] !== '') {
3836
                        $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
3837
                    } else {
3838
                        $res = $this->wrap($res . $linktxt . '</a>', $wrap);
3839
                    }
3840
                    $textstr .= $res . $parts[1];
3841
                } else {
3842
                    $textstr .= $scheme . $textpieces[$i];
3843
                }
3844
            }
3845
            $data = $textstr;
3846
        }
3847
        return $textstr;
3848
    }
3849
3850
    /**
3851
     * Will find all strings prefixed with "mailto:" in the $data string and make them into a link,
3852
     * linking to the email address they point to.
3853
     *
3854
     * @param string $data The string in which to search for "mailto:
3855
     * @param array $conf Configuration for makeLinks, see link
3856
     * @return string The processed input string, being returned.
3857
     * @see _parseFunc()
3858
     */
3859
    public function mailto_makelinks($data, $conf)
3860
    {
3861
        $conf = (array)$conf;
3862
        $parts = [];
3863
        // http-split
3864
        $aTagParams = $this->getATagParams($conf);
3865
        $textpieces = explode('mailto:', $data);
3866
        $pieces = count($textpieces);
3867
        $textstr = $textpieces[0];
3868
        $tsfe = $this->getTypoScriptFrontendController();
3869
        for ($i = 1; $i < $pieces; $i++) {
3870
            $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3871
            if (trim(substr($textstr, -1)) === '' && $len) {
3872
                $lastChar = substr($textpieces[$i], $len - 1, 1);
3873
                if (!preg_match('/[A-Za-z0-9]/', $lastChar)) {
3874
                    $len--;
3875
                }
3876
                $parts[0] = substr($textpieces[$i], 0, $len);
3877
                $parts[1] = substr($textpieces[$i], $len);
3878
                $linktxt = (string)preg_replace('/\\?.*/', '', $parts[0]);
3879
                [$mailToUrl, $linktxt] = $this->getMailTo($parts[0], $linktxt);
3880
                $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl);
3881
                $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>';
3882
                $wrap = (string)$this->stdWrapValue('wrap', $conf);
3883
                if ((string)$conf['ATagBeforeWrap'] !== '') {
3884
                    $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
3885
                } else {
3886
                    $res = $this->wrap($res . $linktxt . '</a>', $wrap);
3887
                }
3888
                $textstr .= $res . $parts[1];
3889
            } else {
3890
                $textstr .= 'mailto:' . $textpieces[$i];
3891
            }
3892
        }
3893
        return $textstr;
3894
    }
3895
3896
    /**
3897
     * Creates and returns a TypoScript "imgResource".
3898
     * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER".
3899
     * In the first case a current image is returned, possibly scaled down or otherwise processed.
3900
     * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines.
3901
     * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']);
3902
     *
3903
     * Structure of the returned info array:
3904
     *  0 => width
3905
     *  1 => height
3906
     *  2 => file extension
3907
     *  3 => file name
3908
     *  origFile => original file name
3909
     *  origFile_mtime => original file mtime
3910
     *  -- only available if processed via FAL: --
3911
     *  originalFile => original file object
3912
     *  processedFile => processed file object
3913
     *  fileCacheHash => checksum of processed file
3914
     *
3915
     * @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.
3916
     * @param array $fileArray TypoScript properties for the imgResource type
3917
     * @return array|null Returns info-array
3918
     * @see cImage()
3919
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder
3920
     */
3921
    public function getImgResource($file, $fileArray)
3922
    {
3923
        $importedFile = null;
3924
        if (empty($file) && empty($fileArray)) {
3925
            return null;
3926
        }
3927
        if (!is_array($fileArray)) {
0 ignored issues
show
introduced by
The condition is_array($fileArray) is always true.
Loading history...
3928
            $fileArray = (array)$fileArray;
3929
        }
3930
        $imageResource = null;
3931
        if ($file === 'GIFBUILDER') {
3932
            $gifCreator = GeneralUtility::makeInstance(GifBuilder::class);
3933
            $theImage = '';
3934
            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) {
3935
                $gifCreator->start($fileArray, $this->data);
3936
                $theImage = $gifCreator->gifBuild();
3937
            }
3938
            $imageResource = $gifCreator->getImageDimensions($theImage);
3939
            $imageResource['origFile'] = $theImage;
3940
        } else {
3941
            if ($file instanceof File) {
3942
                $fileObject = $file;
3943
            } elseif ($file instanceof FileReference) {
3944
                $fileObject = $file->getOriginalFile();
3945
            } else {
3946
                try {
3947
                    if (isset($fileArray['import.']) && $fileArray['import.']) {
3948
                        $importedFile = trim($this->stdWrap('', $fileArray['import.']));
3949
                        if (!empty($importedFile)) {
3950
                            $file = $importedFile;
3951
                        }
3952
                    }
3953
3954
                    if (MathUtility::canBeInterpretedAsInteger($file)) {
3955
                        $treatIdAsReference = $this->stdWrapValue('treatIdAsReference', $fileArray ?? []);
3956
                        if (!empty($treatIdAsReference)) {
3957
                            $file = $this->getResourceFactory()->getFileReferenceObject($file);
3958
                            $fileObject = $file->getOriginalFile();
3959
                        } else {
3960
                            $fileObject = $this->getResourceFactory()->getFileObject($file);
3961
                        }
3962
                    } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier
3963
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
3964
                    } else {
3965
                        if (isset($importedFile) && !empty($importedFile) && !empty($fileArray['import'])) {
3966
                            $file = $fileArray['import'] . $file;
3967
                        }
3968
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
3969
                    }
3970
                } catch (Exception $exception) {
3971
                    $this->logger->warning('The image "' . $file . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]);
3972
                    return null;
3973
                }
3974
            }
3975
            if ($fileObject instanceof File) {
3976
                $processingConfiguration = [];
3977
                $processingConfiguration['width'] = $this->stdWrapValue('width', $fileArray ?? []);
3978
                $processingConfiguration['height'] = $this->stdWrapValue('height', $fileArray ?? []);
3979
                $processingConfiguration['fileExtension'] = $this->stdWrapValue('ext', $fileArray ?? []);
3980
                $processingConfiguration['maxWidth'] = (int)$this->stdWrapValue('maxW', $fileArray ?? []);
3981
                $processingConfiguration['maxHeight'] = (int)$this->stdWrapValue('maxH', $fileArray ?? []);
3982
                $processingConfiguration['minWidth'] = (int)$this->stdWrapValue('minW', $fileArray ?? []);
3983
                $processingConfiguration['minHeight'] = (int)$this->stdWrapValue('minH', $fileArray ?? []);
3984
                $processingConfiguration['noScale'] = $this->stdWrapValue('noScale', $fileArray ?? []);
3985
                $processingConfiguration['additionalParameters'] = $this->stdWrapValue('params', $fileArray ?? []);
3986
                $processingConfiguration['frame'] = (int)$this->stdWrapValue('frame', $fileArray ?? []);
3987
                if ($file instanceof FileReference) {
3988
                    $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($file, $fileArray);
3989
                } else {
3990
                    $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray);
3991
                }
3992
3993
                // Possibility to cancel/force profile extraction
3994
                // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']
3995
                if (isset($fileArray['stripProfile'])) {
3996
                    $processingConfiguration['stripProfile'] = $fileArray['stripProfile'];
3997
                }
3998
                // Check if we can handle this type of file for editing
3999
                if ($fileObject->isImage()) {
4000
                    $maskArray = $fileArray['m.'];
4001
                    // Must render mask images and include in hash-calculating
4002
                    // - otherwise we cannot be sure the filename is unique for the setup!
4003
                    if (is_array($maskArray)) {
4004
                        $mask = $this->getImgResource($maskArray['mask'], $maskArray['mask.']);
4005
                        $bgImg = $this->getImgResource($maskArray['bgImg'], $maskArray['bgImg.']);
4006
                        $bottomImg = $this->getImgResource($maskArray['bottomImg'], $maskArray['bottomImg.']);
4007
                        $bottomImg_mask = $this->getImgResource($maskArray['bottomImg_mask'], $maskArray['bottomImg_mask.']);
4008
4009
                        $processingConfiguration['maskImages']['maskImage'] = $mask['processedFile'];
4010
                        $processingConfiguration['maskImages']['backgroundImage'] = $bgImg['processedFile'];
4011
                        $processingConfiguration['maskImages']['maskBottomImage'] = $bottomImg['processedFile'];
4012
                        $processingConfiguration['maskImages']['maskBottomImageMask'] = $bottomImg_mask['processedFile'];
4013
                    }
4014
                    $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
4015
                    if ($processedFileObject->isProcessed()) {
4016
                        $imageResource = [
4017
                            0 => (int)$processedFileObject->getProperty('width'),
4018
                            1 => (int)$processedFileObject->getProperty('height'),
4019
                            2 => $processedFileObject->getExtension(),
4020
                            3 => $processedFileObject->getPublicUrl(),
4021
                            'origFile' => $fileObject->getPublicUrl(),
4022
                            'origFile_mtime' => $fileObject->getModificationTime(),
4023
                            // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
4024
                            // in order for the setup-array to create a unique filename hash.
4025
                            'originalFile' => $fileObject,
4026
                            'processedFile' => $processedFileObject
4027
                        ];
4028
                    }
4029
                }
4030
            }
4031
        }
4032
        // If image was processed by GIFBUILDER:
4033
        // ($imageResource indicates that it was processed the regular way)
4034
        if (!isset($imageResource)) {
4035
            try {
4036
                $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize((string)$file);
4037
                $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB');
4038
                $info['origFile'] = $theImage;
4039
                // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash.
4040
                $info['origFile_mtime'] = @filemtime($theImage);
4041
                $imageResource = $info;
4042
            } catch (Exception $e) {
4043
                // do nothing in case the file path is invalid
4044
            }
4045
        }
4046
        // Hook 'getImgResource': Post-processing of image resources
4047
        if (isset($imageResource)) {
4048
            /** @var ContentObjectGetImageResourceHookInterface $hookObject */
4049
            foreach ($this->getGetImgResourceHookObjects() as $hookObject) {
4050
                $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this);
4051
            }
4052
        }
4053
        return $imageResource;
4054
    }
4055
4056
    /**
4057
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4058
     * or null if the crop settings or crop area is empty.
4059
     *
4060
     * The cropArea from file reference is used, if not set in TypoScript.
4061
     *
4062
     * Example TypoScript settings:
4063
     * file.crop =
4064
     * OR
4065
     * file.crop = 50,50,100,100
4066
     * OR
4067
     * file.crop.data = file:current:crop
4068
     *
4069
     * @param FileReference $fileReference
4070
     * @param array $fileArray TypoScript properties for the imgResource type
4071
     * @return Area|null
4072
     */
4073
    protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray)
4074
    {
4075
        // Use cropping area from file reference if nothing is configured in TypoScript.
4076
        if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) {
4077
            // Set crop variant from TypoScript settings. If not set, use default.
4078
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4079
            $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant);
4080
            return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference);
4081
        }
4082
4083
        return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray);
4084
    }
4085
4086
    /**
4087
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4088
     * or null if the crop settings or crop area is empty.
4089
     *
4090
     * @param FileInterface $file
4091
     * @param array $fileArray
4092
     * @return Area|null
4093
     */
4094
    protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray)
4095
    {
4096
        /** @var Area $cropArea */
4097
        $cropArea = null;
4098
        // Resolve TypoScript configured cropping.
4099
        $cropSettings = isset($fileArray['crop.'])
4100
            ? $this->stdWrap($fileArray['crop'], $fileArray['crop.'])
4101
            : ($fileArray['crop'] ?? null);
4102
4103
        if (is_string($cropSettings)) {
4104
            // Set crop variant from TypoScript settings. If not set, use default.
4105
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4106
            // Get cropArea from CropVariantCollection, if cropSettings is a valid json.
4107
            // CropVariantCollection::create does json_decode.
4108
            $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant);
4109
            $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file);
4110
4111
            // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100
4112
            if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) {
4113
                $cropSettings = explode(',', $cropSettings);
4114
                if (count($cropSettings) === 4) {
4115
                    $stringCropArea = GeneralUtility::makeInstance(
4116
                        Area::class,
4117
                        ...$cropSettings
4118
                    );
4119
                    $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea;
4120
                }
4121
            }
4122
        }
4123
4124
        return $cropArea;
4125
    }
4126
4127
    /**
4128
     * Takes a JSON string and creates CropVariantCollection and fetches the corresponding
4129
     * CropArea for that.
4130
     *
4131
     * @param string $cropSettings
4132
     * @param string $cropVariant
4133
     * @return Area
4134
     */
4135
    protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area
4136
    {
4137
        return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant);
4138
    }
4139
4140
    /***********************************************
4141
     *
4142
     * Data retrieval etc.
4143
     *
4144
     ***********************************************/
4145
    /**
4146
     * 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.
4147
     *
4148
     * @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)
4149
     * @return string|null
4150
     */
4151
    public function getFieldVal($field)
4152
    {
4153
        if (strpos($field, '//') === false) {
4154
            return $this->data[trim($field)] ?? null;
4155
        }
4156
        $sections = GeneralUtility::trimExplode('//', $field, true);
4157
        foreach ($sections as $k) {
4158
            if ((string)$this->data[$k] !== '') {
4159
                return $this->data[$k];
4160
            }
4161
        }
4162
4163
        return '';
4164
    }
4165
4166
    /**
4167
     * 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.
4168
     *
4169
     * @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)
4170
     * @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.
4171
     * @return string The value fetched
4172
     * @see getFieldVal()
4173
     */
4174
    public function getData($string, $fieldArray = null)
4175
    {
4176
        $tsfe = $this->getTypoScriptFrontendController();
4177
        if (!is_array($fieldArray)) {
4178
            $fieldArray = $tsfe->page;
4179
        }
4180
        $retVal = '';
4181
        $sections = explode('//', $string);
4182
        foreach ($sections as $secKey => $secVal) {
4183
            if ($retVal) {
4184
                break;
4185
            }
4186
            $parts = explode(':', $secVal, 2);
4187
            $type = strtolower(trim($parts[0]));
4188
            $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout'];
4189
            $key = trim($parts[1] ?? '');
4190
            if (($key != '') || in_array($type, $typesWithOutParameters)) {
4191
                switch ($type) {
4192
                    case 'gp':
4193
                        // Merge GET and POST and get $key out of the merged array
4194
                        $getPostArray = GeneralUtility::_GET();
4195
                        ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST());
4196
                        $retVal = $this->getGlobal($key, $getPostArray);
4197
                        break;
4198
                    case 'tsfe':
4199
                        $retVal = $this->getGlobal('TSFE|' . $key);
4200
                        break;
4201
                    case 'getenv':
4202
                        $retVal = getenv($key);
4203
                        break;
4204
                    case 'getindpenv':
4205
                        $retVal = $this->getEnvironmentVariable($key);
4206
                        break;
4207
                    case 'field':
4208
                        $retVal = $this->getGlobal($key, $fieldArray);
4209
                        break;
4210
                    case 'file':
4211
                        $retVal = $this->getFileDataKey($key);
4212
                        break;
4213
                    case 'parameters':
4214
                        $retVal = $this->parameters[$key];
4215
                        break;
4216
                    case 'register':
4217
                        $retVal = $tsfe->register[$key] ?? null;
4218
                        break;
4219
                    case 'global':
4220
                        $retVal = $this->getGlobal($key);
4221
                        break;
4222
                    case 'level':
4223
                        $retVal = count($tsfe->tmpl->rootLine) - 1;
4224
                        break;
4225
                    case 'leveltitle':
4226
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4227
                        $pointer = (int)($keyParts[0] ?? 0);
4228
                        $slide = (string)($keyParts[1] ?? '');
4229
4230
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4231
                        $retVal = $this->rootLineValue($numericKey, 'title', strtolower($slide) === 'slide');
4232
                        break;
4233
                    case 'levelmedia':
4234
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4235
                        $pointer = (int)($keyParts[0] ?? 0);
4236
                        $slide = (string)($keyParts[1] ?? '');
4237
4238
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4239
                        $retVal = $this->rootLineValue($numericKey, 'media', strtolower($slide) === 'slide');
4240
                        break;
4241
                    case 'leveluid':
4242
                        $numericKey = $this->getKey((int)$key, $tsfe->tmpl->rootLine);
4243
                        $retVal = $this->rootLineValue($numericKey, 'uid');
4244
                        break;
4245
                    case 'levelfield':
4246
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4247
                        $pointer = (int)($keyParts[0] ?? 0);
4248
                        $field = (string)($keyParts[1] ?? '');
4249
                        $slide = (string)($keyParts[2] ?? '');
4250
4251
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4252
                        $retVal = $this->rootLineValue($numericKey, $field, strtolower($slide) === 'slide');
4253
                        break;
4254
                    case 'fullrootline':
4255
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4256
                        $pointer = (int)($keyParts[0] ?? 0);
4257
                        $field = (string)($keyParts[1] ?? '');
4258
                        $slide = (string)($keyParts[2] ?? '');
4259
4260
                        $fullKey = (int)($pointer - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine));
4261
                        if ($fullKey >= 0) {
4262
                            $retVal = $this->rootLineValue($fullKey, $field, stristr($slide, 'slide') !== false, $tsfe->rootLine);
4263
                        }
4264
                        break;
4265
                    case 'date':
4266
                        if (!$key) {
4267
                            $key = 'd/m Y';
4268
                        }
4269
                        $retVal = date($key, $GLOBALS['EXEC_TIME']);
4270
                        break;
4271
                    case 'page':
4272
                        $retVal = $tsfe->page[$key];
4273
                        break;
4274
                    case 'pagelayout':
4275
                        $retVal = GeneralUtility::makeInstance(PageLayoutResolver::class)
4276
                            ->getLayoutForPage($tsfe->page, $tsfe->rootLine);
4277
                        break;
4278
                    case 'current':
4279
                        $retVal = $this->data[$this->currentValKey] ?? null;
4280
                        break;
4281
                    case 'db':
4282
                        $selectParts = GeneralUtility::trimExplode(':', $key);
4283
                        $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]);
0 ignored issues
show
Bug introduced by
$selectParts[1] of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Core\Domain\Re...ository::getRawRecord(). ( Ignorable by Annotation )

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

4283
                        $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], /** @scrutinizer ignore-type */ $selectParts[1]);
Loading history...
4284
                        if (is_array($db_rec) && $selectParts[2]) {
4285
                            $retVal = $db_rec[$selectParts[2]];
4286
                        }
4287
                        break;
4288
                    case 'lll':
4289
                        $retVal = $tsfe->sL('LLL:' . $key);
4290
                        break;
4291
                    case 'path':
4292
                        try {
4293
                            $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key);
4294
                        } catch (Exception $e) {
4295
                            // do nothing in case the file path is invalid
4296
                            $retVal = null;
4297
                        }
4298
                        break;
4299
                    case 'cobj':
4300
                        switch ($key) {
4301
                            case 'parentRecordNumber':
4302
                                $retVal = $this->parentRecordNumber;
4303
                                break;
4304
                        }
4305
                        break;
4306
                    case 'debug':
4307
                        switch ($key) {
4308
                            case 'rootLine':
4309
                                $retVal = DebugUtility::viewArray($tsfe->tmpl->rootLine);
4310
                                break;
4311
                            case 'fullRootLine':
4312
                                $retVal = DebugUtility::viewArray($tsfe->rootLine);
4313
                                break;
4314
                            case 'data':
4315
                                $retVal = DebugUtility::viewArray($this->data);
4316
                                break;
4317
                            case 'register':
4318
                                $retVal = DebugUtility::viewArray($tsfe->register);
4319
                                break;
4320
                            case 'page':
4321
                                $retVal = DebugUtility::viewArray($tsfe->page);
4322
                                break;
4323
                        }
4324
                        break;
4325
                    case 'flexform':
4326
                        $keyParts = GeneralUtility::trimExplode(':', $key, true);
4327
                        if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) {
4328
                            $flexFormContent = $this->data[$keyParts[0]];
4329
                            if (!empty($flexFormContent)) {
4330
                                $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
4331
                                $flexFormKey = str_replace('.', '|', $keyParts[1]);
4332
                                $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent);
4333
                                $retVal = $this->getGlobal($flexFormKey, $settings);
4334
                            }
4335
                        }
4336
                        break;
4337
                    case 'session':
4338
                        $keyParts = GeneralUtility::trimExplode('|', $key, true);
4339
                        $sessionKey = array_shift($keyParts);
4340
                        $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey);
4341
                        foreach ($keyParts as $keyPart) {
4342
                            if (is_object($retVal)) {
4343
                                $retVal = $retVal->{$keyPart};
4344
                            } elseif (is_array($retVal)) {
4345
                                $retVal = $retVal[$keyPart];
4346
                            } else {
4347
                                $retVal = '';
4348
                                break;
4349
                            }
4350
                        }
4351
                        if (!is_scalar($retVal)) {
4352
                            $retVal = '';
4353
                        }
4354
                        break;
4355
                    case 'context':
4356
                        $context = GeneralUtility::makeInstance(Context::class);
4357
                        [$aspectName, $propertyName] = GeneralUtility::trimExplode(':', $key, true, 2);
4358
                        $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, '');
4359
                        if (is_array($retVal)) {
4360
                            $retVal = implode(',', $retVal);
4361
                        }
4362
                        if (!is_scalar($retVal)) {
4363
                            $retVal = '';
4364
                        }
4365
                        break;
4366
                    case 'site':
4367
                        $site = $this->getTypoScriptFrontendController()->getSite();
4368
                        if ($key === 'identifier') {
4369
                            $retVal = $site->getIdentifier();
4370
                        } elseif ($key === 'base') {
4371
                            $retVal = $site->getBase();
4372
                        } else {
4373
                            try {
4374
                                $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.');
4375
                            } catch (MissingArrayPathException $exception) {
4376
                                $this->logger->warning(sprintf('getData() with "%s" failed', $key), ['exception' => $exception]);
4377
                            }
4378
                        }
4379
                        break;
4380
                    case 'sitelanguage':
4381
                        $siteLanguage = $this->getTypoScriptFrontendController()->getLanguage();
4382
                        $config = $siteLanguage->toArray();
4383
                        if (isset($config[$key])) {
4384
                            $retVal = $config[$key];
4385
                        }
4386
                        break;
4387
                }
4388
            }
4389
4390
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) {
4391
                $hookObject = GeneralUtility::makeInstance($className);
4392
                if (!$hookObject instanceof ContentObjectGetDataHookInterface) {
4393
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480);
4394
                }
4395
                $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4396
                $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $ref);
4397
            }
4398
        }
4399
        return $retVal;
4400
    }
4401
4402
    /**
4403
     * Gets file information. This is a helper function for the getData() method above, which resolves e.g.
4404
     * page.10.data = file:current:title
4405
     * or
4406
     * page.10.data = file:17:title
4407
     *
4408
     * @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.)
4409
     * @return string|int The value as retrieved from the file object.
4410
     */
4411
    protected function getFileDataKey($key)
4412
    {
4413
        [$fileUidOrCurrentKeyword, $requestedFileInformationKey] = explode(':', $key, 3);
4414
        try {
4415
            if ($fileUidOrCurrentKeyword === 'current') {
4416
                $fileObject = $this->getCurrentFile();
4417
            } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) {
4418
                /** @var ResourceFactory $fileFactory */
4419
                $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
4420
                $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword);
0 ignored issues
show
Bug introduced by
$fileUidOrCurrentKeyword of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Core\Resource\...actory::getFileObject(). ( Ignorable by Annotation )

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

4420
                $fileObject = $fileFactory->getFileObject(/** @scrutinizer ignore-type */ $fileUidOrCurrentKeyword);
Loading history...
4421
            } else {
4422
                $fileObject = null;
4423
            }
4424
        } catch (Exception $exception) {
4425
            $this->logger->warning('The file "' . $fileUidOrCurrentKeyword . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]);
4426
            $fileObject = null;
4427
        }
4428
4429
        if ($fileObject instanceof FileInterface) {
4430
            // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here:
4431
            switch ($requestedFileInformationKey) {
4432
                case 'name':
4433
                    return $fileObject->getName();
4434
                case 'uid':
4435
                    if (method_exists($fileObject, 'getUid')) {
4436
                        return $fileObject->getUid();
4437
                    }
4438
                    return 0;
4439
                case 'originalUid':
4440
                    if ($fileObject instanceof FileReference) {
0 ignored issues
show
introduced by
$fileObject is never a sub-type of TYPO3\CMS\Core\Resource\FileReference.
Loading history...
4441
                        return $fileObject->getOriginalFile()->getUid();
4442
                    }
4443
                    return null;
4444
                case 'size':
4445
                    return $fileObject->getSize();
4446
                case 'sha1':
4447
                    return $fileObject->getSha1();
4448
                case 'extension':
4449
                    return $fileObject->getExtension();
4450
                case 'mimetype':
4451
                    return $fileObject->getMimeType();
4452
                case 'contents':
4453
                    return $fileObject->getContents();
4454
                case 'publicUrl':
4455
                    return $fileObject->getPublicUrl();
4456
                default:
4457
                    // Generic alternative here
4458
                    return $fileObject->getProperty($requestedFileInformationKey);
4459
            }
4460
        } else {
4461
            // @todo fail silently as is common in tslib_content
4462
            return 'Error: no file object';
4463
        }
4464
    }
4465
4466
    /**
4467
     * Returns a value from the current rootline (site) from $GLOBALS['TSFE']->tmpl->rootLine;
4468
     *
4469
     * @param int $key Which level in the root line
4470
     * @param string $field The field in the rootline record to return (a field from the pages table)
4471
     * @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
4472
     * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array
4473
     * @return string The value from the field of the rootline.
4474
     * @internal
4475
     * @see getData()
4476
     */
4477
    public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '')
4478
    {
4479
        $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine;
4480
        if (!$slideBack) {
4481
            return $rootLine[$key][$field];
4482
        }
4483
        for ($a = $key; $a >= 0; $a--) {
4484
            $val = $rootLine[$a][$field];
4485
            if ($val) {
4486
                return $val;
4487
            }
4488
        }
4489
4490
        return '';
4491
    }
4492
4493
    /**
4494
     * Return global variable where the input string $var defines array keys separated by "|"
4495
     * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
4496
     *
4497
     * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
4498
     * @param array $source Alternative array than $GLOBAL to get variables from.
4499
     * @return mixed Whatever value. If none, then blank string.
4500
     * @see getData()
4501
     */
4502
    public function getGlobal($keyString, $source = null)
4503
    {
4504
        $keys = explode('|', $keyString);
4505
        $numberOfLevels = count($keys);
4506
        $rootKey = trim($keys[0]);
4507
        $value = isset($source) ? $source[$rootKey] : $GLOBALS[$rootKey];
4508
        for ($i = 1; $i < $numberOfLevels && isset($value); $i++) {
4509
            $currentKey = trim($keys[$i]);
4510
            if (is_object($value)) {
4511
                $value = $value->{$currentKey};
4512
            } elseif (is_array($value)) {
4513
                $value = $value[$currentKey];
4514
            } else {
4515
                $value = '';
4516
                break;
4517
            }
4518
        }
4519
        if (!is_scalar($value)) {
4520
            $value = '';
4521
        }
4522
        return $value;
4523
    }
4524
4525
    /**
4526
     * 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).
4527
     * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost...
4528
     *
4529
     * @param int $key The integer to transform
4530
     * @param array $arr array in which the key should be found.
4531
     * @return int The processed integer key value.
4532
     * @internal
4533
     * @see getData()
4534
     */
4535
    public function getKey($key, $arr)
4536
    {
4537
        $key = (int)$key;
4538
        if (is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
4539
            if ($key < 0) {
4540
                $key = count($arr) + $key;
4541
            }
4542
            if ($key < 0) {
4543
                $key = 0;
4544
            }
4545
        }
4546
        return $key;
4547
    }
4548
4549
    /***********************************************
4550
     *
4551
     * Link functions (typolink)
4552
     *
4553
     ***********************************************/
4554
    /**
4555
     * called from the typoLink() function
4556
     *
4557
     * does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1"
4558
     * into separate parts
4559
     *
4560
     * @param string $linkText The string (text) to link
4561
     * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
4562
     * @param array $configuration TypoScript configuration
4563
     * @return array|string
4564
     * @see typoLink()
4565
     *
4566
     * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links
4567
     */
4568
    protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
4569
    {
4570
        // Link parameter value = first part
4571
        $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
4572
4573
        // Check for link-handler keyword
4574
        $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2);
4575
        $linkHandlerKeyword = (string)($linkHandlerExploded[0] ?? '');
4576
4577
        if (in_array(strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
4578
            // Disallow insecure scheme's like javascript: or data:
4579
            return $linkText;
4580
        }
4581
4582
        // additional parameters that need to be set
4583
        if ($linkParameterParts['additionalParams'] !== '') {
4584
            $forceParams = $linkParameterParts['additionalParams'];
4585
            // params value
4586
            $configuration['additionalParams'] .= $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
4587
        }
4588
4589
        return [
4590
            'href'   => $linkParameterParts['url'],
4591
            'target' => $linkParameterParts['target'],
4592
            'class'  => $linkParameterParts['class'],
4593
            'title'  => $linkParameterParts['title']
4594
        ];
4595
    }
4596
4597
    /**
4598
     * Implements the "typolink" property of stdWrap (and others)
4599
     * 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.
4600
     * 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.
4601
     * 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.
4602
     * For many more details on the parameters and how they are interpreted, please see the link to TSref below.
4603
     *
4604
     * the FAL API is handled with the namespace/prefix "file:..."
4605
     *
4606
     * @param string $linkText The string (text) to link
4607
     * @param array $conf TypoScript configuration (see link below)
4608
     * @return string A link-wrapped string.
4609
     * @see stdWrap()
4610
     * @see \TYPO3\CMS\Frontend\Plugin\AbstractPlugin::pi_linkTP()
4611
     */
4612
    public function typoLink($linkText, $conf)
4613
    {
4614
        $linkText = (string)$linkText;
4615
        $tsfe = $this->getTypoScriptFrontendController();
4616
4617
        $linkParameter = trim((string)$this->stdWrapValue('parameter', $conf ?? []));
4618
        $this->lastTypoLinkUrl = '';
4619
        $this->lastTypoLinkTarget = '';
4620
4621
        $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf);
4622
        // check if the link handler hook has resolved the link completely already
4623
        if (!is_array($resolvedLinkParameters)) {
4624
            return $resolvedLinkParameters;
4625
        }
4626
        $linkParameter = $resolvedLinkParameters['href'];
4627
        $target = $resolvedLinkParameters['target'];
4628
        $title = $resolvedLinkParameters['title'];
4629
4630
        if (!$linkParameter) {
4631
            return $this->resolveAnchorLink($linkText, $conf ?? []);
4632
        }
4633
4634
        // Detecting kind of link and resolve all necessary parameters
4635
        $linkService = GeneralUtility::makeInstance(LinkService::class);
4636
        try {
4637
            $linkDetails = $linkService->resolve($linkParameter);
4638
        } catch (UnknownLinkHandlerException | InvalidPathException $exception) {
4639
            $this->logger->warning('The link could not be generated', ['exception' => $exception]);
4640
            return $linkText;
4641
        }
4642
4643
        $linkDetails['typoLinkParameter'] = $linkParameter;
4644
        if (isset($linkDetails['type']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
4645
            /** @var AbstractTypolinkBuilder $linkBuilder */
4646
            $linkBuilder = GeneralUtility::makeInstance(
4647
                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
4648
                $this,
4649
                $tsfe
4650
            );
4651
            try {
4652
                [$this->lastTypoLinkUrl, $linkText, $target] = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
4653
                $this->lastTypoLinkTarget = htmlspecialchars($target);
4654
                $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
4655
                $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
4656
            } catch (UnableToLinkException $e) {
4657
                $this->logger->debug(sprintf('Unable to link "%s": %s', $e->getLinkText(), $e->getMessage()), ['exception' => $e]);
4658
4659
                // Only return the link text directly
4660
                return $e->getLinkText();
4661
            }
4662
        } elseif (isset($linkDetails['url'])) {
4663
            $this->lastTypoLinkUrl = $linkDetails['url'];
4664
            $this->lastTypoLinkTarget = htmlspecialchars($target);
4665
            $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
4666
            $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
4667
        } else {
4668
            return $linkText;
4669
        }
4670
4671
        // We need to backup the URL because ATagParams might call typolink again and change the last URL.
4672
        $url = $this->lastTypoLinkUrl;
4673
        $finalTagParts = [
4674
            'aTagParams' => $this->getATagParams($conf),
4675
            'url'        => $url,
4676
            'TYPE'       => $linkDetails['type']
4677
        ];
4678
4679
        // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
4680
        if (!empty($finalTagParts['aTagParams'])) {
4681
            $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'], true);
4682
            if (isset($aTagParams['href'])) {
4683
                unset($aTagParams['href']);
4684
                $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams, true);
4685
            }
4686
        }
4687
4688
        // Building the final <a href=".."> tag
4689
        $tagAttributes = [];
4690
4691
        // Title attribute
4692
        if (empty($title)) {
4693
            $title = $conf['title'] ?? '';
4694
            if (isset($conf['title.']) && is_array($conf['title.'])) {
4695
                $title = $this->stdWrap($title, $conf['title.']);
4696
            }
4697
        }
4698
4699
        // Check, if the target is coded as a JS open window link:
4700
        $JSwindowParts = [];
4701
        $JSwindowParams = '';
4702
        if ($target && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $target, $JSwindowParts)) {
4703
            // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
4704
            $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower(($conf['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
4705
            $JSwindow_paramsArr = [];
4706
            $target = $conf['target'] ?? 'FEopenLink';
4707
            foreach ($JSwindow_tempParamsArr as $JSv) {
4708
                [$JSp, $JSv] = explode('=', $JSv, 2);
4709
                // If the target is set as JS param, this is extracted
4710
                if ($JSp === 'target') {
4711
                    $target = $JSv;
4712
                } else {
4713
                    $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
4714
                }
4715
            }
4716
            // Add width/height:
4717
            $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
4718
            $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
4719
            // Imploding into string:
4720
            $JSwindowParams = implode(',', $JSwindow_paramsArr);
4721
        }
4722
4723
        if (!$JSwindowParams && $linkDetails['type'] === LinkService::TYPE_EMAIL && $tsfe->spamProtectEmailAddresses === 'ascii') {
4724
            $tagAttributes['href'] = $finalTagParts['url'];
4725
        } else {
4726
            $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']);
4727
        }
4728
        if (!empty($title)) {
4729
            $tagAttributes['title'] = htmlspecialchars($title);
4730
        }
4731
4732
        // Target attribute
4733
        if (!empty($target)) {
4734
            $tagAttributes['target'] = htmlspecialchars($target);
4735
        }
4736
        if ($JSwindowParams && in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
4737
            // Create TARGET-attribute only if the right doctype is used
4738
            unset($tagAttributes['target']);
4739
        }
4740
4741
        if ($JSwindowParams) {
4742
            $onClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($finalTagParts['url']))
4743
                . ',' . GeneralUtility::quoteJSvalue($target) . ','
4744
                . GeneralUtility::quoteJSvalue($JSwindowParams)
4745
                . ');vHWin.focus();return false;';
4746
            $tagAttributes['onclick'] = htmlspecialchars($onClick);
4747
        }
4748
4749
        if (!empty($resolvedLinkParameters['class'])) {
4750
            $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']);
4751
        }
4752
4753
        // Prevent trouble with double and missing spaces between attributes and merge params before implode
4754
        // (skip decoding HTML entities, since `$tagAttributes` are expected to be encoded already)
4755
        $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
4756
        $finalTagAttributes = $this->addSecurityRelValues($finalTagAttributes, $target, $tagAttributes['href']);
4757
        $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
4758
4759
        // kept for backwards-compatibility in hooks
4760
        $finalTagParts['targetParams'] = !empty($tagAttributes['target']) ? ' target="' . $tagAttributes['target'] . '"' : '';
4761
        $this->lastTypoLinkTarget = $target;
4762
4763
        // Call user function:
4764
        if ($conf['userFunc'] ?? false) {
4765
            $finalTagParts['TAG'] = $finalAnchorTag;
4766
            $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $finalTagParts);
4767
        }
4768
4769
        // Hook: Call post processing function for link rendering:
4770
        $_params = [
4771
            'conf' => &$conf,
4772
            'linktxt' => &$linkText,
4773
            'finalTag' => &$finalAnchorTag,
4774
            'finalTagParts' => &$finalTagParts,
4775
            'linkDetails' => &$linkDetails,
4776
            'tagAttributes' => &$finalTagAttributes
4777
        ];
4778
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) {
4779
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4780
            GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
4781
        }
4782
4783
        // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made:
4784
        if ($conf['returnLast'] ?? false) {
4785
            switch ($conf['returnLast']) {
4786
                case 'url':
4787
                    return $this->lastTypoLinkUrl;
4788
                case 'target':
4789
                    return $this->lastTypoLinkTarget;
4790
            }
4791
        }
4792
4793
        $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
4794
4795
        if ($conf['ATagBeforeWrap'] ?? false) {
4796
            return $finalAnchorTag . $this->wrap($linkText, $wrap) . '</a>';
4797
        }
4798
        return $this->wrap($finalAnchorTag . $linkText . '</a>', $wrap);
4799
    }
4800
4801
    protected function addSecurityRelValues(array $tagAttributes, ?string $target, string $url): array
4802
    {
4803
        $relAttribute = 'noreferrer';
4804
        if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($url)) {
4805
            return $tagAttributes;
4806
        }
4807
4808
        if (!isset($tagAttributes['rel'])) {
4809
            $tagAttributes['rel'] = $relAttribute;
4810
            return $tagAttributes;
4811
        }
4812
4813
        $tagAttributes['rel'] = implode(' ', array_unique(array_merge(
4814
            GeneralUtility::trimExplode(' ', $relAttribute),
4815
            GeneralUtility::trimExplode(' ', $tagAttributes['rel'])
4816
        )));
4817
4818
        return $tagAttributes;
4819
    }
4820
4821
    /**
4822
     * Checks whether the given url is an internal url.
4823
     *
4824
     * It will check the host part only, against all configured sites
4825
     * whether the given host is any. If so, the url is considered internal
4826
     *
4827
     * @param string $url The url to check.
4828
     * @return bool
4829
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
4830
     */
4831
    protected function isInternalUrl(string $url): bool
4832
    {
4833
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
4834
        $parsedUrl = parse_url($url);
4835
        $foundDomains = 0;
4836
        if (!isset($parsedUrl['host'])) {
4837
            return true;
4838
        }
4839
4840
        $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
4841
4842
        if ($cache->has($cacheIdentifier) === false) {
4843
            foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
4844
                if ($site->getBase()->getHost() === $parsedUrl['host']) {
4845
                    ++$foundDomains;
4846
                    break;
4847
                }
4848
4849
                if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url)) {
4850
                    ++$foundDomains;
4851
                    break;
4852
                }
4853
            }
4854
4855
            $cache->set($cacheIdentifier, $foundDomains > 0);
4856
        }
4857
4858
        return (bool)$cache->get($cacheIdentifier);
4859
    }
4860
4861
    /**
4862
     * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
4863
     *
4864
     * @param array $conf TypoScript properties for "typolink
4865
     * @return string The URL of the link-tag that typolink() would by itself return
4866
     * @see typoLink()
4867
     */
4868
    public function typoLink_URL($conf)
4869
    {
4870
        $this->typoLink('|', $conf);
4871
        return $this->lastTypoLinkUrl;
4872
    }
4873
4874
    /**
4875
     * Returns a linked string made from typoLink parameters.
4876
     *
4877
     * 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.
4878
     * Optionally you can supply $urlParameters which is an array with key/value pairs that are rawurlencoded and appended to the resulting url.
4879
     *
4880
     * @param string $label Text string being wrapped by the link.
4881
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
4882
     * @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.
4883
     * @param string $target Specific target set, if any. (Default is using the current)
4884
     * @return string The wrapped $label-text string
4885
     * @see getTypoLink_URL()
4886
     */
4887
    public function getTypoLink($label, $params, $urlParameters = [], $target = '')
4888
    {
4889
        $conf = [];
4890
        $conf['parameter'] = $params;
4891
        if ($target) {
4892
            $conf['target'] = $target;
4893
            $conf['extTarget'] = $target;
4894
            $conf['fileTarget'] = $target;
4895
        }
4896
        if (is_array($urlParameters)) {
4897
            if (!empty($urlParameters)) {
4898
                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
4899
            }
4900
        } else {
4901
            $conf['additionalParams'] .= $urlParameters;
4902
        }
4903
        $out = $this->typoLink($label, $conf);
4904
        return $out;
4905
    }
4906
4907
    /**
4908
     * Returns the canonical URL to the current "location", which include the current page ID and type
4909
     * and optionally the query string
4910
     *
4911
     * @param bool $addQueryString Whether additional GET arguments in the query string should be included or not
4912
     * @return string
4913
     */
4914
    public function getUrlToCurrentLocation($addQueryString = true)
4915
    {
4916
        $conf = [];
4917
        $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type;
4918
        if ($addQueryString) {
4919
            $conf['addQueryString'] = '1';
4920
            $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars)));
4921
            $conf['addQueryString.'] = [
4922
                'method' => 'GET',
4923
                'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : '')
4924
            ];
4925
        }
4926
4927
        return $this->typoLink_URL($conf);
4928
    }
4929
4930
    /**
4931
     * Returns the URL of a "typolink" create from the input parameter string, url-parameters and target
4932
     *
4933
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
4934
     * @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.
4935
     * @param string $target Specific target set, if any. (Default is using the current)
4936
     * @return string The URL
4937
     * @see getTypoLink()
4938
     */
4939
    public function getTypoLink_URL($params, $urlParameters = [], $target = '')
4940
    {
4941
        $this->getTypoLink('', $params, $urlParameters, $target);
4942
        return $this->lastTypoLinkUrl;
4943
    }
4944
4945
    /**
4946
     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
4947
     *
4948
     * @param string $context The context in which the method is called (e.g. typoLink).
4949
     * @param string $url The URL that should be processed.
4950
     * @param array $typolinkConfiguration The current link configuration array.
4951
     * @return string|null Returns NULL if URL was not processed or the processed URL as a string.
4952
     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
4953
     */
4954
    protected function processUrl($context, $url, $typolinkConfiguration = [])
4955
    {
4956
        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
4957
        if (empty($urlProcessors)) {
4958
            return $url;
4959
        }
4960
4961
        foreach ($urlProcessors as $identifier => $configuration) {
4962
            if (empty($configuration) || !is_array($configuration)) {
4963
                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
4964
            }
4965
            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
4966
                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
4967
            }
4968
        }
4969
4970
        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
4971
        $keepProcessing = true;
4972
4973
        foreach ($orderedProcessors as $configuration) {
4974
            /** @var UrlProcessorInterface $urlProcessor */
4975
            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
4976
            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
4977
            if (!$keepProcessing) {
4978
                break;
4979
            }
4980
        }
4981
4982
        return $url;
4983
    }
4984
4985
    /**
4986
     * Creates a href attibute for given $mailAddress.
4987
     * The function uses spamProtectEmailAddresses for encoding the mailto statement.
4988
     * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:[email protected]".
4989
     *
4990
     * @param string $mailAddress Email address
4991
     * @param string $linktxt Link text, default will be the email address.
4992
     * @return array A numerical array with two elements: 1) $mailToUrl, string ready to be inserted into the href attribute of the <a> tag, b) $linktxt: The string between starting and ending <a> tag.
4993
     */
4994
    public function getMailTo($mailAddress, $linktxt)
4995
    {
4996
        $mailAddress = (string)$mailAddress;
4997
        if ((string)$linktxt === '') {
4998
            $linktxt = htmlspecialchars($mailAddress);
4999
        }
5000
5001
        $originalMailToUrl = 'mailto:' . $mailAddress;
5002
        $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
5003
5004
        // no processing happened, therefore, the default processing kicks in
5005
        if ($mailToUrl === $originalMailToUrl) {
5006
            $tsfe = $this->getTypoScriptFrontendController();
5007
            if ($tsfe->spamProtectEmailAddresses) {
5008
                $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
5009
                if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
5010
                    $encodedForJsAndHref = rawurlencode(GeneralUtility::quoteJSvalue($mailToUrl));
5011
                    $mailToUrl = 'javascript:linkTo_UnCryptMailto(' . $encodedForJsAndHref . ');';
5012
                }
5013
                $atLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst']) ?: '(at)';
5014
                $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
5015
                if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']) {
5016
                    $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
5017
                    $lastDotLabel = $lastDotLabel ?: '(dot)';
5018
                    $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
5019
                    if ($spamProtectedMailAddress === null) {
5020
                        $this->logger->debug('Error replacing the last dot in email address "' . $spamProtectedMailAddress . '"');
0 ignored issues
show
Bug introduced by
Are you sure $spamProtectedMailAddress of type void 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

5020
                        $this->logger->debug('Error replacing the last dot in email address "' . /** @scrutinizer ignore-type */ $spamProtectedMailAddress . '"');
Loading history...
5021
                        $spamProtectedMailAddress = '';
5022
                    }
5023
                }
5024
                $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
5025
            }
5026
        }
5027
5028
        return [$mailToUrl, $linktxt];
5029
    }
5030
5031
    /**
5032
     * Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
5033
     *
5034
     * @param string $string Input string to en/decode: "mailto:[email protected]
5035
     * @param mixed  $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses
5036
     * @return string encoded version of $string
5037
     */
5038
    protected function encryptEmail(string $string, $type): string
5039
    {
5040
        $out = '';
5041
        // obfuscates using the decimal HTML entity references for each character
5042
        if ($type === 'ascii') {
5043
            foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5044
                $out .= '&#' . mb_ord($char) . ';';
0 ignored issues
show
Bug introduced by
The call to mb_ord() has too few arguments starting with encoding. ( Ignorable by Annotation )

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

5044
                $out .= '&#' . /** @scrutinizer ignore-call */ mb_ord($char) . ';';

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
5045
            }
5046
        } else {
5047
            // like str_rot13() but with a variable offset and a wider character range
5048
            $len = strlen($string);
5049
            $offset = (int)$type;
5050
            for ($i = 0; $i < $len; $i++) {
5051
                $charValue = ord($string[$i]);
5052
                // 0-9 . , - + / :
5053
                if ($charValue >= 43 && $charValue <= 58) {
5054
                    $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5055
                } elseif ($charValue >= 64 && $charValue <= 90) {
5056
                    // A-Z @
5057
                    $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5058
                } elseif ($charValue >= 97 && $charValue <= 122) {
5059
                    // a-z
5060
                    $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5061
                } else {
5062
                    $out .= $string[$i];
5063
                }
5064
            }
5065
        }
5066
        return $out;
5067
    }
5068
5069
    /**
5070
     * Encryption (or decryption) of a single character.
5071
     * Within the given range the character is shifted with the supplied offset.
5072
     *
5073
     * @param int $n Ordinal of input character
5074
     * @param int $start Start of range
5075
     * @param int $end End of range
5076
     * @param int $offset Offset
5077
     * @return string encoded/decoded version of character
5078
     */
5079
    protected function encryptCharcode($n, $start, $end, $offset)
5080
    {
5081
        $n = $n + $offset;
5082
        if ($offset > 0 && $n > $end) {
5083
            $n = $start + ($n - $end - 1);
5084
        } elseif ($offset < 0 && $n < $start) {
5085
            $n = $end - ($start - $n - 1);
5086
        }
5087
        return chr($n);
5088
    }
5089
5090
    /**
5091
     * Gets the query arguments and assembles them for URLs.
5092
     * Arguments may be removed or set, depending on configuration.
5093
     *
5094
     * @param array $conf Configuration
5095
     * @param array $overruleQueryArguments Multidimensional key/value pairs that overrule incoming query arguments
5096
     * @param bool $forceOverruleArguments If set, key/value pairs not in the query but the overrule array will be set
5097
     * @return string The URL query part (starting with a &)
5098
     */
5099
    public function getQueryArguments($conf, $overruleQueryArguments = [], $forceOverruleArguments = false)
5100
    {
5101
        $exclude = [];
5102
        $method = (string)($conf['method'] ?? '');
5103
        if ($method === 'GET') {
5104
            $currentQueryArray = GeneralUtility::_GET();
5105
        } else {
5106
            $currentQueryArray = [];
5107
            parse_str($this->getEnvironmentVariable('QUERY_STRING'), $currentQueryArray);
5108
        }
5109
        if ($conf['exclude'] ?? false) {
5110
            $excludeString = str_replace(',', '&', $conf['exclude']);
5111
            $excludedQueryParts = [];
5112
            parse_str($excludeString, $excludedQueryParts);
5113
            // never repeat id
5114
            $exclude['id'] = 0;
5115
            $newQueryArray = ArrayUtility::arrayDiffAssocRecursive($currentQueryArray, $excludedQueryParts);
5116
        } else {
5117
            $newQueryArray = $currentQueryArray;
5118
        }
5119
        ArrayUtility::mergeRecursiveWithOverrule($newQueryArray, $overruleQueryArguments, $forceOverruleArguments);
5120
        return HttpUtility::buildQueryString($newQueryArray, '&');
5121
    }
5122
5123
    /***********************************************
5124
     *
5125
     * Miscellaneous functions, stand alone
5126
     *
5127
     ***********************************************/
5128
    /**
5129
     * Wrapping a string.
5130
     * Implements the TypoScript "wrap" property.
5131
     * Example: $content = "HELLO WORLD" and $wrap = "<strong> | </strong>", result: "<strong>HELLO WORLD</strong>"
5132
     *
5133
     * @param string $content The content to wrap
5134
     * @param string $wrap The wrap value, eg. "<strong> | </strong>
5135
     * @param string $char The char used to split the wrapping value, default is "|
5136
     * @return string Wrapped input string
5137
     * @see noTrimWrap()
5138
     */
5139
    public function wrap($content, $wrap, $char = '|')
5140
    {
5141
        if ($wrap) {
5142
            $wrapArr = explode($char, $wrap);
5143
            $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
5144
        }
5145
        return $content;
5146
    }
5147
5148
    /**
5149
     * Wrapping a string, preserving whitespace in wrap value.
5150
     * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does)
5151
     *
5152
     * @param string $content The content to wrap, eg. "HELLO WORLD
5153
     * @param string $wrap The wrap value, eg. " | <strong> | </strong>
5154
     * @param string $char The char used to split the wrapping value, default is "|"
5155
     * @return string Wrapped input string, eg. " <strong> HELLO WORD </strong>
5156
     * @see wrap()
5157
     */
5158
    public function noTrimWrap($content, $wrap, $char = '|')
5159
    {
5160
        if ($wrap) {
5161
            // expects to be wrapped with (at least) 3 characters (before, middle, after)
5162
            // anything else is not taken into account
5163
            $wrapArr = explode($char, $wrap, 4);
5164
            $content = $wrapArr[1] . $content . $wrapArr[2];
5165
        }
5166
        return $content;
5167
    }
5168
5169
    /**
5170
     * Calling a user function/class-method
5171
     * Notice: For classes the instantiated object will have the internal variable, $cObj, set to be a *reference* to $this (the parent/calling object).
5172
     *
5173
     * @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.
5174
     * @param array $conf The TypoScript configuration to pass the function
5175
     * @param mixed $content The content payload to pass the function
5176
     * @return mixed The return content from the function call. Should probably be a string.
5177
     * @see stdWrap()
5178
     * @see typoLink()
5179
     * @see _parseFunc()
5180
     */
5181
    public function callUserFunction($funcName, $conf, $content)
5182
    {
5183
        // Split parts
5184
        $parts = explode('->', $funcName);
5185
        if (count($parts) === 2) {
5186
            // Check whether PHP class is available
5187
            if (class_exists($parts[0])) {
5188
                if ($this->container && $this->container->has($parts[0])) {
5189
                    $classObj = $this->container->get($parts[0]);
5190
                } else {
5191
                    $classObj = GeneralUtility::makeInstance($parts[0]);
5192
                }
5193
                $methodName = (string)$parts[1];
5194
                $callable = [$classObj, $methodName];
5195
                if (is_object($classObj) && method_exists($classObj, $parts[1]) && is_callable($callable)) {
5196
                    $classObj->cObj = $this;
5197
                    $content = call_user_func_array($callable, [
5198
                        $content,
5199
                        $conf,
5200
                        $this->getRequest()
5201
                    ]);
5202
                } else {
5203
                    $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', 3);
5204
                }
5205
            } else {
5206
                $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', 3);
5207
            }
5208
        } elseif (function_exists($funcName)) {
5209
            $content = $funcName($content, $conf);
5210
        } else {
5211
            $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', 3);
5212
        }
5213
        return $content;
5214
    }
5215
5216
    /**
5217
     * Cleans up a string of keywords. Keywords at splitted by "," (comma)  ";" (semi colon) and linebreak
5218
     *
5219
     * @param string $content String of keywords
5220
     * @return string Cleaned up string, keywords will be separated by a comma only.
5221
     */
5222
    public function keywords($content)
5223
    {
5224
        $listArr = preg_split('/[,;' . LF . ']/', $content);
5225
        if ($listArr === false) {
5226
            return '';
5227
        }
5228
        foreach ($listArr as $k => $v) {
5229
            $listArr[$k] = trim($v);
5230
        }
5231
        return implode(',', $listArr);
5232
    }
5233
5234
    /**
5235
     * Changing character case of a string, converting typically used western charset characters as well.
5236
     *
5237
     * @param string $theValue The string to change case for.
5238
     * @param string $case The direction; either "upper" or "lower
5239
     * @return string
5240
     * @see HTMLcaseshift()
5241
     */
5242
    public function caseshift($theValue, $case)
5243
    {
5244
        switch (strtolower($case)) {
5245
            case 'upper':
5246
                $theValue = mb_strtoupper($theValue, 'utf-8');
5247
                break;
5248
            case 'lower':
5249
                $theValue = mb_strtolower($theValue, 'utf-8');
5250
                break;
5251
            case 'capitalize':
5252
                $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8');
5253
                break;
5254
            case 'ucfirst':
5255
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5256
                $firstChar = mb_strtoupper($firstChar, 'utf-8');
5257
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5258
                $theValue = $firstChar . $remainder;
5259
                break;
5260
            case 'lcfirst':
5261
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5262
                $firstChar = mb_strtolower($firstChar, 'utf-8');
5263
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5264
                $theValue = $firstChar . $remainder;
5265
                break;
5266
            case 'uppercamelcase':
5267
                $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue);
5268
                break;
5269
            case 'lowercamelcase':
5270
                $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue);
5271
                break;
5272
        }
5273
        return $theValue;
5274
    }
5275
5276
    /**
5277
     * Shifts the case of characters outside of HTML tags in the input string
5278
     *
5279
     * @param string $theValue The string to change case for.
5280
     * @param string $case The direction; either "upper" or "lower
5281
     * @return string
5282
     * @see caseshift()
5283
     */
5284
    public function HTMLcaseshift($theValue, $case)
5285
    {
5286
        $inside = 0;
5287
        $newVal = '';
5288
        $pointer = 0;
5289
        $totalLen = strlen($theValue);
5290
        do {
5291
            if (!$inside) {
5292
                $len = strcspn(substr($theValue, $pointer), '<');
5293
                $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case);
5294
                $inside = 1;
5295
            } else {
5296
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
5297
                $newVal .= substr($theValue, $pointer, $len);
5298
                $inside = 0;
5299
            }
5300
            $pointer += $len;
5301
        } while ($pointer < $totalLen);
5302
        return $newVal;
5303
    }
5304
5305
    /**
5306
     * Returns the 'age' of the tstamp $seconds
5307
     *
5308
     * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs
5309
     * @param string $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs'
5310
     * @return string The formatted string
5311
     */
5312
    public function calcAge($seconds, $labels)
5313
    {
5314
        if (MathUtility::canBeInterpretedAsInteger($labels)) {
5315
            $labels = ' min| hrs| days| yrs| min| hour| day| year';
5316
        } else {
5317
            $labels = str_replace('"', '', $labels);
5318
        }
5319
        $labelArr = explode('|', $labels);
5320
        if (count($labelArr) === 4) {
5321
            $labelArr = array_merge($labelArr, $labelArr);
5322
        }
5323
        $absSeconds = abs($seconds);
5324
        $sign = $seconds > 0 ? 1 : -1;
5325
        if ($absSeconds < 3600) {
5326
            $val = round($absSeconds / 60);
5327
            $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]);
5328
        } elseif ($absSeconds < 24 * 3600) {
5329
            $val = round($absSeconds / 3600);
5330
            $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]);
5331
        } elseif ($absSeconds < 365 * 24 * 3600) {
5332
            $val = round($absSeconds / (24 * 3600));
5333
            $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]);
5334
        } else {
5335
            $val = round($absSeconds / (365 * 24 * 3600));
5336
            $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null));
5337
        }
5338
        return $seconds;
5339
    }
5340
5341
    /**
5342
     * Resolves a TypoScript reference value to the full set of properties BUT overridden with any local properties set.
5343
     * So the reference is resolved but overlaid with local TypoScript properties of the reference value.
5344
     *
5345
     * @param array $confArr The TypoScript array
5346
     * @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.
5347
     * @return array The modified TypoScript array
5348
     */
5349
    public function mergeTSRef($confArr, $prop)
5350
    {
5351
        if ($confArr[$prop][0] === '<') {
5352
            $key = trim(substr($confArr[$prop], 1));
5353
            $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
5354
            // $name and $conf is loaded with the referenced values.
5355
            $old_conf = $confArr[$prop . '.'];
5356
            [, $conf] = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
5357
            if (is_array($old_conf) && !empty($old_conf)) {
5358
                $conf = is_array($conf) ? array_replace_recursive($conf, $old_conf) : $old_conf;
5359
            }
5360
            $confArr[$prop . '.'] = $conf;
5361
        }
5362
        return $confArr;
5363
    }
5364
5365
    /***********************************************
5366
     *
5367
     * Database functions, making of queries
5368
     *
5369
     ***********************************************/
5370
    /**
5371
     * Generates a list of Page-uid's from $id. List does not include $id itself
5372
     * (unless the id specified is negative in which case it does!)
5373
     * The only pages WHICH PREVENTS DECENDING in a branch are
5374
     * - deleted pages,
5375
     * - pages in a recycler (doktype = 255) or of the Backend User Section (doktpe = 6) type
5376
     * - pages that has the extendToSubpages set, WHERE start/endtime, hidden
5377
     * and fe_users would hide the records.
5378
     * Apart from that, pages with enable-fields excluding them, will also be
5379
     * removed. HOWEVER $dontCheckEnableFields set will allow
5380
     * enableFields-excluded pages to be included anyway - including
5381
     * extendToSubpages sections!
5382
     * Mount Pages are also descended but notice that these ID numbers are not
5383
     * useful for links unless the correct MPvar is set.
5384
     *
5385
     * @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!
5386
     * @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...)
5387
     * @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'
5388
     * @param bool $dontCheckEnableFields See function description
5389
     * @param string $addSelectFields Additional fields to select. Syntax: ",[fieldname],[fieldname],...
5390
     * @param string $moreWhereClauses Additional where clauses. Syntax: " AND [fieldname]=[value] AND ...
5391
     * @param array $prevId_array array of IDs from previous recursions. In order to prevent infinite loops with mount pages.
5392
     * @param int $recursionLevel Internal: Zero for the first recursion, incremented for each recursive call.
5393
     * @return string Returns the list of ids as a comma separated string
5394
     * @see TypoScriptFrontendController::checkEnableFields()
5395
     * @see TypoScriptFrontendController::checkPagerecordForIncludeSection()
5396
     */
5397
    public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0)
5398
    {
5399
        $id = (int)$id;
5400
        if (!$id) {
5401
            return '';
5402
        }
5403
5404
        // Init vars:
5405
        $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state,l10n_parent' . $addSelectFields;
5406
        $depth = (int)$depth;
5407
        $begin = (int)$begin;
5408
        $theList = [];
5409
        $addId = 0;
5410
        $requestHash = '';
5411
5412
        // First level, check id (second level, this is done BEFORE the recursive call)
5413
        $tsfe = $this->getTypoScriptFrontendController();
5414
        if (!$recursionLevel) {
5415
            // Check tree list cache
5416
            // First, create the hash for this request - not sure yet whether we need all these parameters though
5417
            $parameters = [
5418
                $id,
5419
                $depth,
5420
                $begin,
5421
                $dontCheckEnableFields,
5422
                $addSelectFields,
5423
                $moreWhereClauses,
5424
                $prevId_array,
5425
                GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1])
5426
            ];
5427
            $requestHash = md5(serialize($parameters));
5428
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5429
                ->getQueryBuilderForTable('cache_treelist');
5430
            $cacheEntry = $queryBuilder->select('treelist')
5431
                ->from('cache_treelist')
5432
                ->where(
5433
                    $queryBuilder->expr()->eq(
5434
                        'md5hash',
5435
                        $queryBuilder->createNamedParameter($requestHash, \PDO::PARAM_STR)
5436
                    ),
5437
                    $queryBuilder->expr()->orX(
5438
                        $queryBuilder->expr()->gt(
5439
                            'expires',
5440
                            $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
5441
                        ),
5442
                        $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5443
                    )
5444
                )
5445
                ->setMaxResults(1)
5446
                ->execute()
5447
                ->fetch();
5448
5449
            if (is_array($cacheEntry)) {
5450
                // Cache hit
5451
                return $cacheEntry['treelist'];
5452
            }
5453
            // If Id less than zero it means we should add the real id to list:
5454
            if ($id < 0) {
5455
                $addId = $id = abs($id);
5456
            }
5457
            // Check start page:
5458
            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

5458
            if ($tsfe->sys_page->getRawRecord('pages', /** @scrutinizer ignore-type */ $id, 'uid')) {
Loading history...
5459
                // Find mount point if any:
5460
                $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

5460
                $mount_info = $tsfe->sys_page->getMountPointInfo(/** @scrutinizer ignore-type */ $id);
Loading history...
5461
                if (is_array($mount_info)) {
5462
                    $id = $mount_info['mount_pid'];
5463
                    // In Overlay mode, use the mounted page uid as added ID!:
5464
                    if ($addId && $mount_info['overlay']) {
5465
                        $addId = $id;
5466
                    }
5467
                }
5468
            } else {
5469
                // Return blank if the start page was NOT found at all!
5470
                return '';
5471
            }
5472
        }
5473
        // Add this ID to the array of IDs
5474
        if ($begin <= 0) {
5475
            $prevId_array[] = $id;
5476
        }
5477
        // Select sublevel:
5478
        if ($depth > 0) {
5479
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
5480
            $queryBuilder->getRestrictions()
5481
                ->removeAll()
5482
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5483
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5484
                ->from('pages')
5485
                ->where(
5486
                    $queryBuilder->expr()->eq(
5487
                        'pid',
5488
                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
5489
                    ),
5490
                    // tree is only built by language=0 pages
5491
                    $queryBuilder->expr()->eq('sys_language_uid', 0)
5492
                )
5493
                ->orderBy('sorting');
5494
5495
            if (!empty($moreWhereClauses)) {
5496
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5497
            }
5498
5499
            $result = $queryBuilder->execute();
5500
            while ($row = $result->fetch()) {
5501
                /** @var VersionState $versionState */
5502
                $versionState = VersionState::cast($row['t3ver_state']);
5503
                $tsfe->sys_page->versionOL('pages', $row);
5504
                if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5505
                    || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5506
                    || $versionState->indicatesPlaceholder()
5507
                ) {
5508
                    // Doing this after the overlay to make sure changes
5509
                    // in the overlay are respected.
5510
                    // However, we do not process pages below of and
5511
                    // including of type recycler and BE user section
5512
                    continue;
5513
                }
5514
                // Find mount point if any:
5515
                $next_id = $row['uid'];
5516
                $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row);
5517
                // Overlay mode:
5518
                if (is_array($mount_info) && $mount_info['overlay']) {
5519
                    $next_id = $mount_info['mount_pid'];
5520
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5521
                        ->getQueryBuilderForTable('pages');
5522
                    $queryBuilder->getRestrictions()
5523
                        ->removeAll()
5524
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5525
                    $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5526
                        ->from('pages')
5527
                        ->where(
5528
                            $queryBuilder->expr()->eq(
5529
                                'uid',
5530
                                $queryBuilder->createNamedParameter($next_id, \PDO::PARAM_INT)
5531
                            )
5532
                        )
5533
                        ->orderBy('sorting')
5534
                        ->setMaxResults(1);
5535
5536
                    if (!empty($moreWhereClauses)) {
5537
                        $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5538
                    }
5539
5540
                    $row = $queryBuilder->execute()->fetch();
5541
                    $tsfe->sys_page->versionOL('pages', $row);
5542
                    if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5543
                        || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5544
                        || $versionState->indicatesPlaceholder()
5545
                    ) {
5546
                        // Doing this after the overlay to make sure
5547
                        // changes in the overlay are respected.
5548
                        // see above
5549
                        continue;
5550
                    }
5551
                }
5552
                // Add record:
5553
                if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) {
5554
                    // Add ID to list:
5555
                    if ($begin <= 0) {
5556
                        if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) {
5557
                            $theList[] = $next_id;
5558
                        }
5559
                    }
5560
                    // Next level:
5561
                    if ($depth > 1 && !$row['php_tree_stop']) {
5562
                        // Normal mode:
5563
                        if (is_array($mount_info) && !$mount_info['overlay']) {
5564
                            $next_id = $mount_info['mount_pid'];
5565
                        }
5566
                        // Call recursively, if the id is not in prevID_array:
5567
                        if (!in_array($next_id, $prevId_array)) {
5568
                            $theList = array_merge(
5569
                                GeneralUtility::intExplode(
5570
                                    ',',
5571
                                    $this->getTreeList(
5572
                                        $next_id,
5573
                                        $depth - 1,
5574
                                        $begin - 1,
5575
                                        $dontCheckEnableFields,
5576
                                        $addSelectFields,
5577
                                        $moreWhereClauses,
5578
                                        $prevId_array,
5579
                                        $recursionLevel + 1
5580
                                    ),
5581
                                    true
5582
                                ),
5583
                                $theList
5584
                            );
5585
                        }
5586
                    }
5587
                }
5588
            }
5589
        }
5590
        // If first run, check if the ID should be returned:
5591
        if (!$recursionLevel) {
5592
            if ($addId) {
5593
                if ($begin > 0) {
5594
                    $theList[] = 0;
5595
                } else {
5596
                    $theList[] = $addId;
5597
                }
5598
            }
5599
5600
            $cacheEntry = [
5601
                'md5hash' => $requestHash,
5602
                'pid' => $id,
5603
                'treelist' => implode(',', $theList),
5604
                'tstamp' => $GLOBALS['EXEC_TIME'],
5605
            ];
5606
5607
            // Only add to cache if not logged into TYPO3 Backend
5608
            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...
5609
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
5610
                try {
5611
                    $connection->transactional(function ($connection) use ($cacheEntry) {
5612
                        $connection->insert('cache_treelist', $cacheEntry);
5613
                    });
5614
                } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
5615
                }
5616
            }
5617
        }
5618
5619
        return implode(',', $theList);
5620
    }
5621
5622
    /**
5623
     * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.)
5624
     * 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%")'
5625
     *
5626
     * @param string $searchWords The search words. These will be separated by space and comma.
5627
     * @param string $searchFieldList The fields to search in
5628
     * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well)
5629
     * @return string The WHERE clause.
5630
     */
5631
    public function searchWhere($searchWords, $searchFieldList, $searchTable)
5632
    {
5633
        if (!$searchWords) {
5634
            return '';
5635
        }
5636
5637
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5638
            ->getQueryBuilderForTable($searchTable);
5639
5640
        $prefixTableName = $searchTable ? $searchTable . '.' : '';
5641
5642
        $where = $queryBuilder->expr()->andX();
5643
        $searchFields = explode(',', $searchFieldList);
5644
        $searchWords = preg_split('/[ ,]/', $searchWords);
5645
        foreach ($searchWords as $searchWord) {
5646
            $searchWord = trim($searchWord);
5647
            if (strlen($searchWord) < 3) {
5648
                continue;
5649
            }
5650
            $searchWordConstraint = $queryBuilder->expr()->orX();
5651
            $searchWord = $queryBuilder->escapeLikeWildcards($searchWord);
5652
            foreach ($searchFields as $field) {
5653
                $searchWordConstraint->add(
5654
                    $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%'))
5655
                );
5656
            }
5657
5658
            if ($searchWordConstraint->count()) {
5659
                $where->add($searchWordConstraint);
5660
            }
5661
        }
5662
5663
        if ((string)$where === '') {
5664
            return '';
5665
        }
5666
5667
        return ' AND (' . (string)$where . ')';
5668
    }
5669
5670
    /**
5671
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5672
     * This function is preferred over ->getQuery() if you just need to create and then execute a query.
5673
     *
5674
     * @param string $table The table name
5675
     * @param array $conf The TypoScript configuration properties
5676
     * @return Statement
5677
     * @see getQuery()
5678
     */
5679
    public function exec_getQuery($table, $conf)
5680
    {
5681
        $statement = $this->getQuery($table, $conf);
5682
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5683
5684
        return $connection->executeQuery($statement);
5685
    }
5686
5687
    /**
5688
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5689
     * and overlays with translation and version if available
5690
     *
5691
     * @param string $tableName the name of the TCA database table
5692
     * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference
5693
     * @return array The records
5694
     * @throws \UnexpectedValueException
5695
     */
5696
    public function getRecords($tableName, array $queryConfiguration)
5697
    {
5698
        $records = [];
5699
5700
        $statement = $this->exec_getQuery($tableName, $queryConfiguration);
5701
5702
        $tsfe = $this->getTypoScriptFrontendController();
5703
        while ($row = $statement->fetch()) {
5704
            // Versioning preview:
5705
            $tsfe->sys_page->versionOL($tableName, $row, true);
5706
5707
            // Language overlay:
5708
            if (is_array($row)) {
5709
                $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row);
5710
            }
5711
5712
            // Might be unset in the language overlay
5713
            if (is_array($row)) {
5714
                $records[] = $row;
5715
            }
5716
        }
5717
5718
        return $records;
5719
    }
5720
5721
    /**
5722
     * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5723
     * Implements the "select" function in TypoScript
5724
     *
5725
     * @param string $table See ->exec_getQuery()
5726
     * @param array $conf See ->exec_getQuery()
5727
     * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED!
5728
     * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts.
5729
     * @throws \RuntimeException
5730
     * @throws \InvalidArgumentException
5731
     * @internal
5732
     * @see numRows()
5733
     */
5734
    public function getQuery($table, $conf, $returnQueryArray = false)
5735
    {
5736
        // Resolve stdWrap in these properties first
5737
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5738
        $properties = [
5739
            'pidInList',
5740
            'uidInList',
5741
            'languageField',
5742
            'selectFields',
5743
            'max',
5744
            'begin',
5745
            'groupBy',
5746
            'orderBy',
5747
            'join',
5748
            'leftjoin',
5749
            'rightjoin',
5750
            'recursive',
5751
            'where'
5752
        ];
5753
        foreach ($properties as $property) {
5754
            $conf[$property] = trim(
5755
                isset($conf[$property . '.'])
5756
                    ? $this->stdWrap($conf[$property], $conf[$property . '.'])
5757
                    : $conf[$property]
5758
            );
5759
            if ($conf[$property] === '') {
5760
                unset($conf[$property]);
5761
            } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftJoin', 'rightJoin', 'where'], true)) {
5762
                $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]);
5763
            }
5764
            if (isset($conf[$property . '.'])) {
5765
                // stdWrapping already done, so remove the sub-array
5766
                unset($conf[$property . '.']);
5767
            }
5768
        }
5769
        // Handle PDO-style named parameter markers first
5770
        $queryMarkers = $this->getQueryMarkers($table, $conf);
5771
        // Replace the markers in the non-stdWrap properties
5772
        foreach ($queryMarkers as $marker => $markerValue) {
5773
            $properties = [
5774
                'uidInList',
5775
                'selectFields',
5776
                'where',
5777
                'max',
5778
                'begin',
5779
                'groupBy',
5780
                'orderBy',
5781
                'join',
5782
                'leftjoin',
5783
                'rightjoin'
5784
            ];
5785
            foreach ($properties as $property) {
5786
                if ($conf[$property]) {
5787
                    $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]);
5788
                }
5789
            }
5790
        }
5791
5792
        // Construct WHERE clause:
5793
        // Handle recursive function for the pidInList
5794
        if (isset($conf['recursive'])) {
5795
            $conf['recursive'] = (int)$conf['recursive'];
5796
            if ($conf['recursive'] > 0) {
5797
                $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true);
5798
                array_walk($pidList, function (&$storagePid) {
5799
                    if ($storagePid === 'this') {
5800
                        $storagePid = $this->getTypoScriptFrontendController()->id;
5801
                    }
5802
                    if ($storagePid > 0) {
5803
                        $storagePid = -$storagePid;
5804
                    }
5805
                });
5806
                $expandedPidList = [];
5807
                foreach ($pidList as $value) {
5808
                    // Implementation of getTreeList allows to pass the id negative to include
5809
                    // it into the result otherwise only childpages are returned
5810
                    $expandedPidList = array_merge(
5811
                        GeneralUtility::intExplode(',', $this->getTreeList((int)$value, (int)($conf['recursive'] ?? 0))),
5812
                        $expandedPidList
5813
                    );
5814
                }
5815
                $conf['pidInList'] = implode(',', $expandedPidList);
5816
            }
5817
        }
5818
        if ((string)$conf['pidInList'] === '') {
5819
            $conf['pidInList'] = 'this';
5820
        }
5821
5822
        $queryParts = $this->getQueryConstraints($table, $conf);
5823
5824
        $queryBuilder = $connection->createQueryBuilder();
5825
        // @todo Check against getQueryConstraints, can probably use FrontendRestrictions
5826
        // @todo here and remove enableFields there.
5827
        $queryBuilder->getRestrictions()->removeAll();
5828
        $queryBuilder->select('*')->from($table);
5829
5830
        if ($queryParts['where']) {
5831
            $queryBuilder->where($queryParts['where']);
5832
        }
5833
5834
        if ($queryParts['groupBy']) {
5835
            $queryBuilder->groupBy(...$queryParts['groupBy']);
5836
        }
5837
5838
        if (is_array($queryParts['orderBy'])) {
5839
            foreach ($queryParts['orderBy'] as $orderBy) {
5840
                $queryBuilder->addOrderBy(...$orderBy);
5841
            }
5842
        }
5843
5844
        // Fields:
5845
        if ($conf['selectFields']) {
5846
            $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table));
5847
        }
5848
5849
        // Setting LIMIT:
5850
        $error = false;
5851
        if ($conf['max'] || $conf['begin']) {
5852
            // Finding the total number of records, if used:
5853
            if (strpos(strtolower($conf['begin'] . $conf['max']), 'total') !== false) {
5854
                $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5855
                $countQueryBuilder->getRestrictions()->removeAll();
5856
                $countQueryBuilder->count('*')
5857
                    ->from($table)
5858
                    ->where($queryParts['where']);
5859
5860
                if ($queryParts['groupBy']) {
5861
                    $countQueryBuilder->groupBy(...$queryParts['groupBy']);
5862
                }
5863
5864
                try {
5865
                    $count = $countQueryBuilder->execute()->fetchColumn(0);
5866
                    $conf['max'] = str_ireplace('total', $count, $conf['max']);
5867
                    $conf['begin'] = str_ireplace('total', $count, $conf['begin']);
5868
                } catch (DBALException $e) {
5869
                    $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage());
5870
                    $error = true;
5871
                }
5872
            }
5873
5874
            if (!$error) {
5875
                $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'])), 0);
5876
                $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'])), 0);
5877
                if ($conf['begin'] > 0) {
5878
                    $queryBuilder->setFirstResult($conf['begin']);
5879
                }
5880
                $queryBuilder->setMaxResults($conf['max'] ?: 100000);
5881
            }
5882
        }
5883
5884
        if (!$error) {
5885
            // Setting up tablejoins:
5886
            if ($conf['join']) {
5887
                $joinParts = QueryHelper::parseJoin($conf['join']);
5888
                $queryBuilder->join(
5889
                    $table,
5890
                    $joinParts['tableName'],
5891
                    $joinParts['tableAlias'],
5892
                    $joinParts['joinCondition']
5893
                );
5894
            } elseif ($conf['leftjoin']) {
5895
                $joinParts = QueryHelper::parseJoin($conf['leftjoin']);
5896
                $queryBuilder->leftJoin(
5897
                    $table,
5898
                    $joinParts['tableName'],
5899
                    $joinParts['tableAlias'],
5900
                    $joinParts['joinCondition']
5901
                );
5902
            } elseif ($conf['rightjoin']) {
5903
                $joinParts = QueryHelper::parseJoin($conf['rightjoin']);
5904
                $queryBuilder->rightJoin(
5905
                    $table,
5906
                    $joinParts['tableName'],
5907
                    $joinParts['tableAlias'],
5908
                    $joinParts['joinCondition']
5909
                );
5910
            }
5911
5912
            // Convert the QueryBuilder object into a SQL statement.
5913
            $query = $queryBuilder->getSQL();
5914
5915
            // Replace the markers in the queryParts to handle stdWrap enabled properties
5916
            foreach ($queryMarkers as $marker => $markerValue) {
5917
                // @todo Ugly hack that needs to be cleaned up, with the current architecture
5918
                // @todo for exec_Query / getQuery it's the best we can do.
5919
                $query = str_replace('###' . $marker . '###', $markerValue, $query);
5920
                foreach ($queryParts as $queryPartKey => &$queryPartValue) {
5921
                    $queryPartValue = str_replace('###' . $marker . '###', $markerValue, $queryPartValue);
5922
                }
5923
                unset($queryPartValue);
5924
            }
5925
5926
            return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
5927
        }
5928
5929
        return '';
5930
    }
5931
5932
    /**
5933
     * Helper to transform a QueryBuilder object into a queryParts array that can be used
5934
     * with exec_SELECT_queryArray
5935
     *
5936
     * @param \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder
5937
     * @return array
5938
     * @throws \RuntimeException
5939
     */
5940
    protected function getQueryArray(QueryBuilder $queryBuilder)
5941
    {
5942
        $fromClauses = [];
5943
        $knownAliases = [];
5944
        $queryParts = [];
5945
5946
        // Loop through all FROM clauses
5947
        foreach ($queryBuilder->getQueryPart('from') as $from) {
5948
            if ($from['alias'] === null) {
5949
                $tableSql = $from['table'];
5950
                $tableReference = $from['table'];
5951
            } else {
5952
                $tableSql = $from['table'] . ' ' . $from['alias'];
5953
                $tableReference = $from['alias'];
5954
            }
5955
5956
            $knownAliases[$tableReference] = true;
5957
5958
            $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
5959
                $tableReference,
5960
                $queryBuilder->getQueryPart('join'),
5961
                $knownAliases
5962
            );
5963
        }
5964
5965
        $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
5966
        $queryParts['FROM'] = implode(', ', $fromClauses);
5967
        $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
5968
        $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
5969
        $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
5970
        if ($queryBuilder->getFirstResult() > 0) {
5971
            $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
5972
        } elseif ($queryBuilder->getMaxResults() > 0) {
5973
            $queryParts['LIMIT'] = $queryBuilder->getMaxResults();
5974
        }
5975
5976
        return $queryParts;
5977
    }
5978
5979
    /**
5980
     * Helper to transform the QueryBuilder join part into a SQL fragment.
5981
     *
5982
     * @param string $fromAlias
5983
     * @param array $joinParts
5984
     * @param array $knownAliases
5985
     * @return string
5986
     * @throws \RuntimeException
5987
     */
5988
    protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
5989
    {
5990
        $sql = '';
5991
5992
        if (isset($joinParts['join'][$fromAlias])) {
5993
            foreach ($joinParts['join'][$fromAlias] as $join) {
5994
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
5995
                    throw new \RuntimeException(
5996
                        'Non unique join alias: "' . $join['joinAlias'] . '" found.',
5997
                        1472748872
5998
                    );
5999
                }
6000
                $sql .= ' ' . strtoupper($join['joinType'])
6001
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
6002
                    . ' ON ' . ((string)$join['joinCondition']);
6003
                $knownAliases[$join['joinAlias']] = true;
6004
            }
6005
6006
            foreach ($joinParts['join'][$fromAlias] as $join) {
6007
                $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
6008
            }
6009
        }
6010
6011
        return $sql;
6012
    }
6013
    /**
6014
     * Helper function for getQuery(), creating the WHERE clause of the SELECT query
6015
     *
6016
     * @param string $table The table name
6017
     * @param array $conf The TypoScript configuration properties
6018
     * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments
6019
     * @throws \InvalidArgumentException
6020
     * @see getQuery()
6021
     */
6022
    protected function getQueryConstraints(string $table, array $conf): array
6023
    {
6024
        // Init:
6025
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6026
        $expressionBuilder = $queryBuilder->expr();
6027
        $tsfe = $this->getTypoScriptFrontendController();
6028
        $constraints = [];
6029
        $pid_uid_flag = 0;
6030
        $enableFieldsIgnore = [];
6031
        $queryParts = [
6032
            'where' => null,
6033
            'groupBy' => null,
6034
            'orderBy' => null,
6035
        ];
6036
6037
        $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
6038
        $considerMovePointers = (
6039
            $isInWorkspace && $table !== 'pages'
6040
            && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
6041
        );
6042
6043
        if (trim($conf['uidInList'])) {
6044
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['uidInList']));
6045
6046
            // If moved records shall be considered, select via t3ver_oid
6047
            if ($considerMovePointers) {
6048
                $constraints[] = (string)$expressionBuilder->orX(
6049
                    $expressionBuilder->in($table . '.uid', $listArr),
6050
                    $expressionBuilder->andX(
6051
                        $expressionBuilder->eq(
6052
                            $table . '.t3ver_state',
6053
                            (int)(string)VersionState::cast(VersionState::MOVE_POINTER)
6054
                        ),
6055
                        $expressionBuilder->in($table . '.t3ver_oid', $listArr)
6056
                    )
6057
                );
6058
            } else {
6059
                $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr);
6060
            }
6061
            $pid_uid_flag++;
6062
        }
6063
6064
        // Static_* tables are allowed to be fetched from root page
6065
        if (strpos($table, 'static_') === 0) {
6066
            $pid_uid_flag++;
6067
        }
6068
6069
        if (trim($conf['pidInList'])) {
6070
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['pidInList']));
6071
            // Removes all pages which are not visible for the user!
6072
            $listArr = $this->checkPidArray($listArr);
6073
            if (GeneralUtility::inList($conf['pidInList'], 'root')) {
6074
                $listArr[] = 0;
6075
            }
6076
            if (GeneralUtility::inList($conf['pidInList'], '-1')) {
6077
                $listArr[] = -1;
6078
                $enableFieldsIgnore['pid'] = true;
6079
            }
6080
            if (!empty($listArr)) {
6081
                $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr));
6082
                $pid_uid_flag++;
6083
            } else {
6084
                // If not uid and not pid then uid is set to 0 - which results in nothing!!
6085
                $pid_uid_flag = 0;
6086
            }
6087
        }
6088
6089
        // If not uid and not pid then uid is set to 0 - which results in nothing!!
6090
        if (!$pid_uid_flag) {
6091
            $constraints[] = $expressionBuilder->eq($table . '.uid', 0);
6092
        }
6093
6094
        $where = trim((string)$this->stdWrapValue('where', $conf ?? []));
6095
        if ($where) {
6096
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
6097
        }
6098
6099
        // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched
6100
        // but only do this for TCA tables that have languages enabled
6101
        $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class));
6102
        if ($languageConstraint !== null) {
6103
            $constraints[] = $languageConstraint;
6104
        }
6105
6106
        // Enablefields
6107
        if ($table === 'pages') {
6108
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
6109
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
6110
        } else {
6111
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
6112
        }
6113
6114
        // MAKE WHERE:
6115
        if (count($constraints) !== 0) {
6116
            $queryParts['where'] = $expressionBuilder->andX(...$constraints);
6117
        }
6118
        // GROUP BY
6119
        $groupBy = trim((string)$this->stdWrapValue('groupBy', $conf ?? []));
6120
        if ($groupBy) {
6121
            $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy);
6122
        }
6123
6124
        // ORDER BY
6125
        $orderByString = trim((string)$this->stdWrapValue('orderBy', $conf ?? []));
6126
        if ($orderByString) {
6127
            $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString);
6128
        }
6129
6130
        // Return result:
6131
        return $queryParts;
6132
    }
6133
6134
    /**
6135
     * Adds parts to the WHERE clause that are related to language.
6136
     * This only works on TCA tables which have the [ctrl][languageField] field set or if they
6137
     * have select.languageField = my_language_field set explicitly.
6138
     *
6139
     * It is also possible to disable the language restriction for a query by using select.languageField = 0,
6140
     * if select.languageField is not explicitly set, the TCA default values are taken.
6141
     *
6142
     * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted:
6143
     *
6144
     * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are
6145
     * fetched (the overlays are taken care of later-on).
6146
     * if the current language has overlays but also records without localization-parent (free mode) available,
6147
     * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1
6148
     * which overrules the overlayType within the language aspect.
6149
     *
6150
     * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records
6151
     * for the current language.
6152
     *
6153
     * @param ExpressionBuilder $expressionBuilder
6154
     * @param string $table
6155
     * @param array $conf
6156
     * @param Context $context
6157
     * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null
6158
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
6159
     */
6160
    protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
6161
    {
6162
        $languageField = '';
6163
        $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
6164
        // Check if the table is translatable, and set the language field by default from the TCA information
6165
        if (!empty($conf['languageField']) || !isset($conf['languageField'])) {
6166
            if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) {
6167
                $languageField = $conf['languageField'];
6168
            } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) {
6169
                $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
6170
            }
6171
        }
6172
6173
        // No language restriction enabled explicitly or available via TCA
6174
        if (empty($languageField)) {
6175
            return null;
6176
        }
6177
6178
        /** @var LanguageAspect $languageAspect */
6179
        $languageAspect = $context->getAspect('language');
6180
        if ($languageAspect->doOverlays() && !empty($localizationParentField)) {
6181
            // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will
6182
            // OVERLAY the records with localized versions!
6183
            $languageQuery = $expressionBuilder->in($languageField, [0, -1]);
6184
            // Use this option to include records that don't have a default language counterpart ("free mode")
6185
            // (originalpointerfield is 0 and the language field contains the requested language)
6186
            if (isset($conf['includeRecordsWithoutDefaultTranslation']) || $conf['includeRecordsWithoutDefaultTranslation.']) {
6187
                $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ?
6188
                    $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation'];
6189
                $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== '';
6190
            } else {
6191
                // Option was not explicitly set, check what's in for the language overlay type.
6192
                $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING;
6193
            }
6194
            if ($includeRecordsWithoutDefaultTranslation) {
6195
                $languageQuery = $expressionBuilder->orX(
6196
                    $languageQuery,
6197
                    $expressionBuilder->andX(
6198
                        $expressionBuilder->eq($table . '.' . $localizationParentField, 0),
6199
                        $expressionBuilder->eq($languageField, $languageAspect->getContentId())
6200
                    )
6201
                );
6202
            }
6203
            return $languageQuery;
6204
        }
6205
        // No overlays = only fetch records given for the requested language and "all languages"
6206
        return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]);
6207
    }
6208
6209
    /**
6210
     * Helper function for getQuery, sanitizing the select part
6211
     *
6212
     * This functions checks if the necessary fields are part of the select
6213
     * and adds them if necessary.
6214
     *
6215
     * @param string $selectPart Select part
6216
     * @param string $table Table to select from
6217
     * @return string Sanitized select part
6218
     * @internal
6219
     * @see getQuery
6220
     */
6221
    protected function sanitizeSelectPart($selectPart, $table)
6222
    {
6223
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6224
6225
        // Pattern matching parts
6226
        $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
6227
        $matchEnd = '(\\s*,|\\s*$)/';
6228
        $necessaryFields = ['uid', 'pid'];
6229
        $wsFields = ['t3ver_state'];
6230
        if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) {
6231
            foreach ($necessaryFields as $field) {
6232
                $match = $matchStart . $field . $matchEnd;
6233
                if (!preg_match($match, $selectPart)) {
6234
                    $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6235
                }
6236
            }
6237
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
6238
                foreach ($wsFields as $field) {
6239
                    $match = $matchStart . $field . $matchEnd;
6240
                    if (!preg_match($match, $selectPart)) {
6241
                        $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6242
                    }
6243
                }
6244
            }
6245
        }
6246
        return $selectPart;
6247
    }
6248
6249
    /**
6250
     * 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)
6251
     *
6252
     * @param int[] $pageIds Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed.
6253
     * @return array Returns the array of remaining page UID numbers
6254
     * @internal
6255
     */
6256
    public function checkPidArray($pageIds)
6257
    {
6258
        if (!is_array($pageIds) || empty($pageIds)) {
0 ignored issues
show
introduced by
The condition is_array($pageIds) is always true.
Loading history...
6259
            return [];
6260
        }
6261
        $restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class);
6262
        $restrictionContainer->add(GeneralUtility::makeInstance(
6263
            DocumentTypeExclusionRestriction::class,
6264
            GeneralUtility::intExplode(',', (string)$this->checkPid_badDoktypeList, true)
6265
        ));
6266
        return $this->getTypoScriptFrontendController()->sys_page->filterAccessiblePageIds($pageIds, $restrictionContainer);
6267
    }
6268
6269
    /**
6270
     * Builds list of marker values for handling PDO-like parameter markers in select parts.
6271
     * 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.
6272
     *
6273
     * @param string $table Table to select records from
6274
     * @param array $conf Select part of CONTENT definition
6275
     * @return array List of values to replace markers with
6276
     * @internal
6277
     * @see getQuery()
6278
     */
6279
    public function getQueryMarkers($table, $conf)
6280
    {
6281
        if (!is_array($conf['markers.'])) {
6282
            return [];
6283
        }
6284
        // Parse markers and prepare their values
6285
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6286
        $markerValues = [];
6287
        foreach ($conf['markers.'] as $dottedMarker => $dummy) {
6288
            $marker = rtrim($dottedMarker, '.');
6289
            if ($dottedMarker != $marker . '.') {
6290
                continue;
6291
            }
6292
            // Parse definition
6293
            // todo else value is always null
6294
            $tempValue = isset($conf['markers.'][$dottedMarker])
6295
                ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'], $conf['markers.'][$dottedMarker])
6296
                : $conf['markers.'][$dottedMarker]['value'];
6297
            // Quote/escape if needed
6298
            if (is_numeric($tempValue)) {
6299
                if ((int)$tempValue == $tempValue) {
6300
                    // Handle integer
6301
                    $markerValues[$marker] = (int)$tempValue;
6302
                } else {
6303
                    // Handle float
6304
                    $markerValues[$marker] = (float)$tempValue;
6305
                }
6306
            } elseif ($tempValue === null) {
6307
                // It represents NULL
6308
                $markerValues[$marker] = 'NULL';
6309
            } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
6310
                // See if it is really a comma separated list of values
6311
                $explodeValues = GeneralUtility::trimExplode(',', $tempValue);
6312
                if (count($explodeValues) > 1) {
6313
                    // Handle each element of list separately
6314
                    $tempArray = [];
6315
                    foreach ($explodeValues as $listValue) {
6316
                        if (is_numeric($listValue)) {
6317
                            if ((int)$listValue == $listValue) {
6318
                                $tempArray[] = (int)$listValue;
6319
                            } else {
6320
                                $tempArray[] = (float)$listValue;
6321
                            }
6322
                        } else {
6323
                            // If quoted, remove quotes before
6324
                            // escaping.
6325
                            if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
6326
                                $listValue = $matches[1];
6327
                            } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
6328
                                $listValue = $matches[1];
6329
                            }
6330
                            $tempArray[] = $connection->quote($listValue);
6331
                        }
6332
                    }
6333
                    $markerValues[$marker] = implode(',', $tempArray);
6334
                } else {
6335
                    // Handle remaining values as string
6336
                    $markerValues[$marker] = $connection->quote($tempValue);
6337
                }
6338
            } else {
6339
                // Handle remaining values as string
6340
                $markerValues[$marker] = $connection->quote($tempValue);
6341
            }
6342
        }
6343
        return $markerValues;
6344
    }
6345
6346
    /***********************************************
6347
     *
6348
     * Frontend editing functions
6349
     *
6350
     ***********************************************/
6351
    /**
6352
     * 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.
6353
     * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
6354
     * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
6355
     *
6356
     * @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.
6357
     * @param array $conf TypoScript configuration properties for the editPanel
6358
     * @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
6359
     * @param array $dataArray Alternative data array to use. Default is $this->data
6360
     * @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.
6361
     */
6362
    public function editPanel($content, $conf, $currentRecord = '', $dataArray = [])
6363
    {
6364
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6365
            return $content;
6366
        }
6367
        if (!$this->getTypoScriptFrontendController()->displayEditIcons) {
6368
            return $content;
6369
        }
6370
6371
        if (!$currentRecord) {
6372
            $currentRecord = $this->currentRecord;
6373
        }
6374
        if (empty($dataArray)) {
6375
            $dataArray = $this->data;
6376
        }
6377
6378
        if ($conf['newRecordFromTable']) {
6379
            $currentRecord = $conf['newRecordFromTable'] . ':NEW';
6380
            $conf['allow'] = 'new';
6381
            $checkEditAccessInternals = false;
6382
        } else {
6383
            $checkEditAccessInternals = true;
6384
        }
6385
        [$table, $uid] = explode(':', $currentRecord);
6386
        // Page ID for new records, 0 if not specified
6387
        $newRecordPid = (int)$conf['newRecordInPid'];
6388
        $newUid = null;
6389
        if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) {
6390
            if ($table === 'pages') {
6391
                $newUid = $uid;
6392
            } else {
6393
                if ($conf['newRecordFromTable']) {
6394
                    $newUid = $this->getTypoScriptFrontendController()->id;
6395
                    if ($newRecordPid) {
6396
                        $newUid = $newRecordPid;
6397
                    }
6398
                } else {
6399
                    $newUid = -1 * $uid;
6400
                }
6401
            }
6402
        }
6403
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6404
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6405
            if ($editClass) {
6406
                $edit = GeneralUtility::makeInstance($editClass);
6407
                $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']);
6408
                $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []);
6409
            }
6410
        }
6411
        return $content;
6412
    }
6413
6414
    /**
6415
     * 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.
6416
     * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
6417
     *
6418
     * @param string $content The content to which the edit icons should be appended
6419
     * @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
6420
     * @param array $conf TypoScript properties for configuring the edit icons.
6421
     * @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
6422
     * @param array $dataArray Alternative data array to use. Default is $this->data
6423
     * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine
6424
     * @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.
6425
     */
6426
    public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '')
6427
    {
6428
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6429
            return $content;
6430
        }
6431
        if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) {
6432
            return $content;
6433
        }
6434
        if (!$currentRecord) {
6435
            $currentRecord = $this->currentRecord;
6436
        }
6437
        if (empty($dataArray)) {
6438
            $dataArray = $this->data;
6439
        }
6440
        // Check incoming params:
6441
        [$currentRecordTable, $currentRecordUID] = explode(':', $currentRecord);
6442
        [$fieldList, $table] = array_reverse(GeneralUtility::trimExplode(':', $params, true));
6443
        // Reverse the array because table is optional
6444
        if (!$table) {
6445
            $table = $currentRecordTable;
6446
        } elseif ($table != $currentRecordTable) {
6447
            // If the table is set as the first parameter, and does not match the table of the current record, then just return.
6448
            return $content;
6449
        }
6450
6451
        $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID;
6452
        // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
6453
        if (!array_key_exists('allow', $conf)) {
6454
            $conf['allow'] = 'edit';
6455
        }
6456
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6457
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6458
            if ($editClass) {
6459
                $edit = GeneralUtility::makeInstance($editClass);
6460
                $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
6461
            }
6462
        }
6463
        return $content;
6464
    }
6465
6466
    /**
6467
     * Returns TRUE if the input table/row would be hidden in the frontend (according nto the current time and simulate user group)
6468
     *
6469
     * @param string $table The table name
6470
     * @param array $row The data record
6471
     * @return bool
6472
     * @internal
6473
     * @see editPanelPreviewBorder()
6474
     */
6475
    public function isDisabled($table, $row)
6476
    {
6477
        $tsfe = $this->getTypoScriptFrontendController();
6478
        $enablecolumns = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
6479
        return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']]
6480
            || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup
6481
            || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > $GLOBALS['EXEC_TIME']
6482
            || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < $GLOBALS['EXEC_TIME'];
6483
    }
6484
6485
    /**
6486
     * Get instance of FAL resource factory
6487
     *
6488
     * @return ResourceFactory
6489
     */
6490
    protected function getResourceFactory()
6491
    {
6492
        return GeneralUtility::makeInstance(ResourceFactory::class);
6493
    }
6494
6495
    /**
6496
     * Wrapper function for GeneralUtility::getIndpEnv()
6497
     *
6498
     * @see GeneralUtility::getIndpEnv
6499
     * @param string $key Name of the "environment variable"/"server variable" you wish to get.
6500
     * @return string
6501
     */
6502
    protected function getEnvironmentVariable($key)
6503
    {
6504
        return GeneralUtility::getIndpEnv($key);
6505
    }
6506
6507
    /**
6508
     * Fetches content from cache
6509
     *
6510
     * @param array $configuration Array
6511
     * @return string|bool FALSE on cache miss
6512
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
6513
     */
6514
    protected function getFromCache(array $configuration)
6515
    {
6516
        $content = false;
6517
6518
        if ($this->getTypoScriptFrontendController()->no_cache) {
6519
            return $content;
6520
        }
6521
        $cacheKey = $this->calculateCacheKey($configuration);
6522
        if (!empty($cacheKey)) {
6523
            /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
6524
            $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)
6525
                ->getCache('hash');
6526
            $content = $cacheFrontend->get($cacheKey);
6527
        }
6528
        return $content;
6529
    }
6530
6531
    /**
6532
     * Calculates the lifetime of a cache entry based on the given configuration
6533
     *
6534
     * @param array $configuration
6535
     * @return int|null
6536
     */
6537
    protected function calculateCacheLifetime(array $configuration)
6538
    {
6539
        $configuration['lifetime'] = $configuration['lifetime'] ?? '';
6540
        $lifetimeConfiguration = (string)$this->stdWrapValue('lifetime', $configuration);
6541
6542
        $lifetime = null; // default lifetime
6543
        if (strtolower($lifetimeConfiguration) === 'unlimited') {
6544
            $lifetime = 0; // unlimited
6545
        } elseif ($lifetimeConfiguration > 0) {
6546
            $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds
6547
        }
6548
        return $lifetime;
6549
    }
6550
6551
    /**
6552
     * Calculates the tags for a cache entry bases on the given configuration
6553
     *
6554
     * @param array $configuration
6555
     * @return array
6556
     */
6557
    protected function calculateCacheTags(array $configuration)
6558
    {
6559
        $configuration['tags'] = $configuration['tags'] ?? '';
6560
        $tags = (string)$this->stdWrapValue('tags', $configuration);
6561
        return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags);
6562
    }
6563
6564
    /**
6565
     * Applies stdWrap to the cache key
6566
     *
6567
     * @param array $configuration
6568
     * @return string
6569
     */
6570
    protected function calculateCacheKey(array $configuration)
6571
    {
6572
        $configuration['key'] = $configuration['key'] ?? '';
6573
        return $this->stdWrapValue('key', $configuration);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->stdWrapValue('key', $configuration) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
6574
    }
6575
6576
    /**
6577
     * Returns the current BE user.
6578
     *
6579
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
6580
     */
6581
    protected function getFrontendBackendUser()
6582
    {
6583
        return $GLOBALS['BE_USER'];
6584
    }
6585
6586
    /**
6587
     * @return TimeTracker
6588
     */
6589
    protected function getTimeTracker()
6590
    {
6591
        return GeneralUtility::makeInstance(TimeTracker::class);
6592
    }
6593
6594
    /**
6595
     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
6596
     */
6597
    protected function getTypoScriptFrontendController()
6598
    {
6599
        return $this->typoScriptFrontendController ?: $GLOBALS['TSFE'];
6600
    }
6601
6602
    /**
6603
     * Support anchors without href value
6604
     * Changes ContentObjectRenderer::typolink to render a tag without href,
6605
     * if id or name attribute is present.
6606
     *
6607
     * @param string $linkText
6608
     * @param array $conf Typolink configuration decoded as array
6609
     * @return string Full a-Tag or just the linktext if id or name are not set.
6610
     */
6611
    protected function resolveAnchorLink(string $linkText, array $conf): string
6612
    {
6613
        $anchorTag = '<a ' . $this->getATagParams($conf) . '>';
6614
        $aTagParams = GeneralUtility::get_tag_attributes($anchorTag);
6615
        // If it looks like a anchor tag, render it anyway
6616
        if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
6617
            return $anchorTag . $linkText . '</a>';
6618
        }
6619
        // Otherwise just return the link text
6620
        return $linkText;
6621
    }
6622
6623
    /**
6624
     * Get content length of the current tag that could also contain nested tag contents
6625
     *
6626
     * @param string $theValue
6627
     * @param int $pointer
6628
     * @param string $currentTag
6629
     * @return int
6630
     */
6631
    protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int
6632
    {
6633
        $tempContent = strtolower(substr($theValue, $pointer));
6634
        $startTag = '<' . $currentTag;
6635
        $endTag = '</' . $currentTag . '>';
6636
        $offsetCount = 0;
6637
6638
        // Take care for nested tags
6639
        do {
6640
            $nextMatchingEndTagPosition = strpos($tempContent, $endTag);
6641
            // only match tag `a` in `<a href"...">` but not in `<abbr>`
6642
            $nextSameTypeTagPosition = preg_match(
6643
                '#' . $startTag . '[\s/>]#',
6644
                $tempContent,
6645
                $nextSameStartTagMatches,
6646
                PREG_OFFSET_CAPTURE
6647
            ) ? $nextSameStartTagMatches[0][1] : false;
6648
6649
            // filter out nested tag contents to help getting the correct closing tag
6650
            if ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) {
6651
                $lastOpeningTagStartPosition = (int)strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag);
6652
                $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag);
6653
                $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition;
6654
6655
                // replace content from latest tag start to latest tag end
6656
                $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition);
6657
            }
6658
        } while (
6659
            ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) &&
6660
            $nextSameTypeTagPosition < $nextMatchingEndTagPosition
6661
        );
6662
6663
        // if no closing tag is found we use length of the whole content
6664
        $endingOffset = strlen($tempContent);
6665
        if ($nextMatchingEndTagPosition !== false) {
6666
            $endingOffset = $nextMatchingEndTagPosition + $offsetCount;
6667
        }
6668
6669
        return $endingOffset;
6670
    }
6671
6672
    public function getRequest(): ServerRequestInterface
6673
    {
6674
        if ($this->request instanceof ServerRequestInterface) {
6675
            return $this->request;
6676
        }
6677
6678
        if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
6679
            return $GLOBALS['TYPO3_REQUEST'];
6680
        }
6681
6682
        throw new ContentRenderingException('PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972);
6683
    }
6684
}
6685