ContentObjectRenderer::encaps_lineSplit()   F
last analyzed

Complexity

Conditions 39
Paths 3939

Size

Total Lines 105
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 39
eloc 70
nc 3939
nop 2
dl 0
loc 105
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

1954
        $content = !empty($conf['date.']['GMT']) ? gmdate(/** @scrutinizer ignore-type */ $conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
Loading history...
Bug introduced by
It seems like $conf['date'] ?? null can also be of type null; however, parameter $format of date() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1954
        $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date(/** @scrutinizer ignore-type */ $conf['date'] ?? null, $content);
Loading history...
1955
        return $content;
1956
    }
1957
1958
    /**
1959
     * strftime
1960
     * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties
1961
     * Will return gmstrftime when the property GMT returns TRUE
1962
     *
1963
     * @param string $content Input value undergoing processing in this function.
1964
     * @param array $conf stdWrap properties for strftime.
1965
     * @return string The processed input value
1966
     */
1967
    public function stdWrap_strftime($content = '', $conf = [])
1968
    {
1969
        // Check for zero length string to mimic default case of strtime/gmstrftime
1970
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
1971
        $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT'])
1972
            ? gmstrftime($conf['strftime'] ?? null, $content)
0 ignored issues
show
Bug introduced by
It seems like $conf['strftime'] ?? null can also be of type null; however, parameter $format of gmstrftime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1972
            ? gmstrftime(/** @scrutinizer ignore-type */ $conf['strftime'] ?? null, $content)
Loading history...
1973
            : strftime($conf['strftime'] ?? null, $content);
0 ignored issues
show
Bug introduced by
It seems like $conf['strftime'] ?? null can also be of type null; however, parameter $format of strftime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1973
            : strftime(/** @scrutinizer ignore-type */ $conf['strftime'] ?? null, $content);
Loading history...
1974
        if (!empty($conf['strftime.']['charset'])) {
1975
            $output = mb_convert_encoding((string)$content, 'utf-8', trim(strtolower($conf['strftime.']['charset'])));
1976
            return $output ?: $content;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $output ?: $content also could return the type array which is incompatible with the documented return type string.
Loading history...
1977
        }
1978
        return $content;
1979
    }
1980
1981
    /**
1982
     * strtotime
1983
     * Will return a timestamp based on configuration given according to PHP strtotime
1984
     *
1985
     * @param string $content Input value undergoing processing in this function.
1986
     * @param array $conf stdWrap properties for strtotime.
1987
     * @return string The processed input value
1988
     */
1989
    public function stdWrap_strtotime($content = '', $conf = [])
1990
    {
1991
        if ($conf['strtotime'] !== '1') {
1992
            $content .= ' ' . $conf['strtotime'];
1993
        }
1994
        return strtotime($content, $GLOBALS['EXEC_TIME']);
1995
    }
1996
1997
    /**
1998
     * age
1999
     * Will return the age of a given timestamp based on configuration given by stdWrap properties
2000
     *
2001
     * @param string $content Input value undergoing processing in this function.
2002
     * @param array $conf stdWrap properties for age.
2003
     * @return string The processed input value
2004
     */
2005
    public function stdWrap_age($content = '', $conf = [])
2006
    {
2007
        return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null);
2008
    }
2009
2010
    /**
2011
     * case
2012
     * Will transform the content to be upper or lower case only
2013
     * Leaves HTML tags untouched
2014
     *
2015
     * @param string $content Input value undergoing processing in this function.
2016
     * @param array $conf stdWrap properties for case.
2017
     * @return string The processed input value
2018
     */
2019
    public function stdWrap_case($content = '', $conf = [])
2020
    {
2021
        return $this->HTMLcaseshift($content, $conf['case']);
2022
    }
2023
2024
    /**
2025
     * bytes
2026
     * Will return the size of a given number in Bytes	 *
2027
     *
2028
     * @param string $content Input value undergoing processing in this function.
2029
     * @param array $conf stdWrap properties for bytes.
2030
     * @return string The processed input value
2031
     */
2032
    public function stdWrap_bytes($content = '', $conf = [])
2033
    {
2034
        return GeneralUtility::formatSize((int)$content, $conf['bytes.']['labels'], $conf['bytes.']['base']);
2035
    }
2036
2037
    /**
2038
     * substring
2039
     * Will return a substring based on position information given by stdWrap properties
2040
     *
2041
     * @param string $content Input value undergoing processing in this function.
2042
     * @param array $conf stdWrap properties for substring.
2043
     * @return string The processed input value
2044
     */
2045
    public function stdWrap_substring($content = '', $conf = [])
2046
    {
2047
        return $this->substring($content, $conf['substring']);
2048
    }
2049
2050
    /**
2051
     * cropHTML
2052
     * Crops content to a given size while leaving HTML tags untouched
2053
     *
2054
     * @param string $content Input value undergoing processing in this function.
2055
     * @param array $conf stdWrap properties for cropHTML.
2056
     * @return string The processed input value
2057
     */
2058
    public function stdWrap_cropHTML($content = '', $conf = [])
2059
    {
2060
        return $this->cropHTML($content, $conf['cropHTML'] ?? '');
2061
    }
2062
2063
    /**
2064
     * stripHtml
2065
     * Completely removes HTML tags from content
2066
     *
2067
     * @param string $content Input value undergoing processing in this function.
2068
     * @return string The processed input value
2069
     */
2070
    public function stdWrap_stripHtml($content = '')
2071
    {
2072
        return strip_tags($content);
2073
    }
2074
2075
    /**
2076
     * crop
2077
     * Crops content to a given size without caring about HTML tags
2078
     *
2079
     * @param string $content Input value undergoing processing in this function.
2080
     * @param array $conf stdWrap properties for crop.
2081
     * @return string The processed input value
2082
     */
2083
    public function stdWrap_crop($content = '', $conf = [])
2084
    {
2085
        return $this->crop($content, $conf['crop']);
2086
    }
2087
2088
    /**
2089
     * rawUrlEncode
2090
     * Encodes content to be used within URLs
2091
     *
2092
     * @param string $content Input value undergoing processing in this function.
2093
     * @return string The processed input value
2094
     */
2095
    public function stdWrap_rawUrlEncode($content = '')
2096
    {
2097
        return rawurlencode($content);
2098
    }
2099
2100
    /**
2101
     * htmlSpecialChars
2102
     * Transforms HTML tags to readable text by replacing special characters with their HTML entity
2103
     * When preserveEntities returns TRUE, existing entities will be left untouched
2104
     *
2105
     * @param string $content Input value undergoing processing in this function.
2106
     * @param array $conf stdWrap properties for htmlSpecialChars.
2107
     * @return string The processed input value
2108
     */
2109
    public function stdWrap_htmlSpecialChars($content = '', $conf = [])
2110
    {
2111
        if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) {
2112
            $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false);
2113
        } else {
2114
            $content = htmlspecialchars($content);
2115
        }
2116
        return $content;
2117
    }
2118
2119
    /**
2120
     * encodeForJavaScriptValue
2121
     * Escapes content to be used inside JavaScript strings. Single quotes are added around the value.
2122
     *
2123
     * @param string $content Input value undergoing processing in this function
2124
     * @return string The processed input value
2125
     */
2126
    public function stdWrap_encodeForJavaScriptValue($content = '')
2127
    {
2128
        return GeneralUtility::quoteJSvalue($content);
2129
    }
2130
2131
    /**
2132
     * doubleBrTag
2133
     * Searches for double line breaks and replaces them with the given value
2134
     *
2135
     * @param string $content Input value undergoing processing in this function.
2136
     * @param array $conf stdWrap properties for doubleBrTag.
2137
     * @return string The processed input value
2138
     */
2139
    public function stdWrap_doubleBrTag($content = '', $conf = [])
2140
    {
2141
        return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? null, $content);
2142
    }
2143
2144
    /**
2145
     * br
2146
     * Searches for single line breaks and replaces them with a <br />/<br> tag
2147
     * according to the doctype
2148
     *
2149
     * @param string $content Input value undergoing processing in this function.
2150
     * @return string The processed input value
2151
     */
2152
    public function stdWrap_br($content = '')
2153
    {
2154
        return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype));
2155
    }
2156
2157
    /**
2158
     * brTag
2159
     * Searches for single line feeds and replaces them with the given value
2160
     *
2161
     * @param string $content Input value undergoing processing in this function.
2162
     * @param array $conf stdWrap properties for brTag.
2163
     * @return string The processed input value
2164
     */
2165
    public function stdWrap_brTag($content = '', $conf = [])
2166
    {
2167
        return str_replace(LF, $conf['brTag'] ?? null, $content);
2168
    }
2169
2170
    /**
2171
     * encapsLines
2172
     * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet
2173
     * and wrapping them with values given by stdWrap properties
2174
     *
2175
     * @param string $content Input value undergoing processing in this function.
2176
     * @param array $conf stdWrap properties for erncapsLines.
2177
     * @return string The processed input value
2178
     */
2179
    public function stdWrap_encapsLines($content = '', $conf = [])
2180
    {
2181
        return $this->encaps_lineSplit($content, $conf['encapsLines.']);
2182
    }
2183
2184
    /**
2185
     * keywords
2186
     * Transforms content into a CSV list to be used i.e. as keywords within a meta tag
2187
     *
2188
     * @param string $content Input value undergoing processing in this function.
2189
     * @return string The processed input value
2190
     */
2191
    public function stdWrap_keywords($content = '')
2192
    {
2193
        return $this->keywords($content);
2194
    }
2195
2196
    /**
2197
     * innerWrap
2198
     * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2199
     * See wrap
2200
     *
2201
     * @param string $content Input value undergoing processing in this function.
2202
     * @param array $conf stdWrap properties for innerWrap.
2203
     * @return string The processed input value
2204
     */
2205
    public function stdWrap_innerWrap($content = '', $conf = [])
2206
    {
2207
        return $this->wrap($content, $conf['innerWrap'] ?? null);
2208
    }
2209
2210
    /**
2211
     * innerWrap2
2212
     * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2213
     * See wrap
2214
     *
2215
     * @param string $content Input value undergoing processing in this function.
2216
     * @param array $conf stdWrap properties for innerWrap2.
2217
     * @return string The processed input value
2218
     */
2219
    public function stdWrap_innerWrap2($content = '', $conf = [])
2220
    {
2221
        return $this->wrap($content, $conf['innerWrap2'] ?? null);
2222
    }
2223
2224
    /**
2225
     * preCObject
2226
     * A content object that is prepended to the current content but between the innerWraps and the rest of the wraps
2227
     *
2228
     * @param string $content Input value undergoing processing in this function.
2229
     * @param array $conf stdWrap properties for preCObject.
2230
     * @return string The processed input value
2231
     */
2232
    public function stdWrap_preCObject($content = '', $conf = [])
2233
    {
2234
        return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content;
2235
    }
2236
2237
    /**
2238
     * postCObject
2239
     * A content object that is appended to the current content but between the innerWraps and the rest of the wraps
2240
     *
2241
     * @param string $content Input value undergoing processing in this function.
2242
     * @param array $conf stdWrap properties for postCObject.
2243
     * @return string The processed input value
2244
     */
2245
    public function stdWrap_postCObject($content = '', $conf = [])
2246
    {
2247
        return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject');
2248
    }
2249
2250
    /**
2251
     * wrapAlign
2252
     * Wraps content with a div container having the style attribute text-align set to the given value
2253
     * See wrap
2254
     *
2255
     * @param string $content Input value undergoing processing in this function.
2256
     * @param array $conf stdWrap properties for wrapAlign.
2257
     * @return string The processed input value
2258
     */
2259
    public function stdWrap_wrapAlign($content = '', $conf = [])
2260
    {
2261
        $wrapAlign = trim($conf['wrapAlign'] ?? '');
2262
        if ($wrapAlign) {
2263
            $content = $this->wrap($content, '<div style="text-align:' . htmlspecialchars($wrapAlign) . ';">|</div>');
2264
        }
2265
        return $content;
2266
    }
2267
2268
    /**
2269
     * typolink
2270
     * Wraps the content with a link tag
2271
     * URLs and other attributes are created automatically by the values given in the stdWrap properties
2272
     * See wrap
2273
     *
2274
     * @param string $content Input value undergoing processing in this function.
2275
     * @param array $conf stdWrap properties for typolink.
2276
     * @return string The processed input value
2277
     */
2278
    public function stdWrap_typolink($content = '', $conf = [])
2279
    {
2280
        return $this->typoLink($content, $conf['typolink.']);
2281
    }
2282
2283
    /**
2284
     * wrap
2285
     * This is the "mother" of all wraps
2286
     * Third of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2287
     * Basically it will put additional content before and after the current content using a split character as a placeholder for the current content
2288
     * The default split character is | but it can be replaced with other characters by the property splitChar
2289
     * Any other wrap that does not have own splitChar settings will be using the default split char though
2290
     *
2291
     * @param string $content Input value undergoing processing in this function.
2292
     * @param array $conf stdWrap properties for wrap.
2293
     * @return string The processed input value
2294
     */
2295
    public function stdWrap_wrap($content = '', $conf = [])
2296
    {
2297
        return $this->wrap(
2298
            $content,
2299
            $conf['wrap'] ?? null,
2300
            $conf['wrap.']['splitChar'] ?? '|'
2301
        );
2302
    }
2303
2304
    /**
2305
     * noTrimWrap
2306
     * Fourth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2307
     * The major difference to any other wrap is, that this one can make use of whitespace without trimming	 *
2308
     *
2309
     * @param string $content Input value undergoing processing in this function.
2310
     * @param array $conf stdWrap properties for noTrimWrap.
2311
     * @return string The processed input value
2312
     */
2313
    public function stdWrap_noTrimWrap($content = '', $conf = [])
2314
    {
2315
        $splitChar = isset($conf['noTrimWrap.']['splitChar.'])
2316
            ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.'])
2317
            : $conf['noTrimWrap.']['splitChar'] ?? '';
2318
        if ($splitChar === null || $splitChar === '') {
2319
            $splitChar = '|';
2320
        }
2321
        $content = $this->noTrimWrap(
2322
            $content,
2323
            $conf['noTrimWrap'],
2324
            $splitChar
2325
        );
2326
        return $content;
2327
    }
2328
2329
    /**
2330
     * wrap2
2331
     * Fifth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2332
     * The default split character is | but it can be replaced with other characters by the property splitChar
2333
     *
2334
     * @param string $content Input value undergoing processing in this function.
2335
     * @param array $conf stdWrap properties for wrap2.
2336
     * @return string The processed input value
2337
     */
2338
    public function stdWrap_wrap2($content = '', $conf = [])
2339
    {
2340
        return $this->wrap(
2341
            $content,
2342
            $conf['wrap2'] ?? null,
2343
            $conf['wrap2.']['splitChar'] ?? '|'
2344
        );
2345
    }
2346
2347
    /**
2348
     * dataWrap
2349
     * Sixth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2350
     * 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
2351
     *
2352
     * @param string $content Input value undergoing processing in this function.
2353
     * @param array $conf stdWrap properties for dataWrap.
2354
     * @return string The processed input value
2355
     */
2356
    public function stdWrap_dataWrap($content = '', $conf = [])
2357
    {
2358
        return $this->dataWrap($content, $conf['dataWrap']);
2359
    }
2360
2361
    /**
2362
     * prepend
2363
     * A content object that will be prepended to the current content after most of the wraps have already been applied
2364
     *
2365
     * @param string $content Input value undergoing processing in this function.
2366
     * @param array $conf stdWrap properties for prepend.
2367
     * @return string The processed input value
2368
     */
2369
    public function stdWrap_prepend($content = '', $conf = [])
2370
    {
2371
        return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content;
2372
    }
2373
2374
    /**
2375
     * append
2376
     * A content object that will be appended to the current content after most of the wraps have already been applied
2377
     *
2378
     * @param string $content Input value undergoing processing in this function.
2379
     * @param array $conf stdWrap properties for append.
2380
     * @return string The processed input value
2381
     */
2382
    public function stdWrap_append($content = '', $conf = [])
2383
    {
2384
        return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append');
2385
    }
2386
2387
    /**
2388
     * wrap3
2389
     * Seventh of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2390
     * The default split character is | but it can be replaced with other characters by the property splitChar
2391
     *
2392
     * @param string $content Input value undergoing processing in this function.
2393
     * @param array $conf stdWrap properties for wrap3.
2394
     * @return string The processed input value
2395
     */
2396
    public function stdWrap_wrap3($content = '', $conf = [])
2397
    {
2398
        return $this->wrap(
2399
            $content,
2400
            $conf['wrap3'] ?? null,
2401
            $conf['wrap3.']['splitChar'] ?? '|'
2402
        );
2403
    }
2404
2405
    /**
2406
     * orderedStdWrap
2407
     * Calls stdWrap for each entry in the provided array
2408
     *
2409
     * @param string $content Input value undergoing processing in this function.
2410
     * @param array $conf stdWrap properties for orderedStdWrap.
2411
     * @return string The processed input value
2412
     */
2413
    public function stdWrap_orderedStdWrap($content = '', $conf = [])
2414
    {
2415
        $sortedKeysArray = ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true);
2416
        foreach ($sortedKeysArray as $key) {
2417
            $content = $this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null);
2418
        }
2419
        return $content;
2420
    }
2421
2422
    /**
2423
     * outerWrap
2424
     * Eighth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2425
     *
2426
     * @param string $content Input value undergoing processing in this function.
2427
     * @param array $conf stdWrap properties for outerWrap.
2428
     * @return string The processed input value
2429
     */
2430
    public function stdWrap_outerWrap($content = '', $conf = [])
2431
    {
2432
        return $this->wrap($content, $conf['outerWrap'] ?? null);
2433
    }
2434
2435
    /**
2436
     * insertData
2437
     * Can fetch additional content the same way data does and replaces any occurrence of {field:whatever} with this content
2438
     *
2439
     * @param string $content Input value undergoing processing in this function.
2440
     * @return string The processed input value
2441
     */
2442
    public function stdWrap_insertData($content = '')
2443
    {
2444
        return $this->insertData($content);
2445
    }
2446
2447
    /**
2448
     * postUserFunc
2449
     * Will execute a user function after the content has been modified by any other stdWrap function
2450
     *
2451
     * @param string $content Input value undergoing processing in this function.
2452
     * @param array $conf stdWrap properties for postUserFunc.
2453
     * @return string The processed input value
2454
     */
2455
    public function stdWrap_postUserFunc($content = '', $conf = [])
2456
    {
2457
        return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'] ?? [], $content);
2458
    }
2459
2460
    /**
2461
     * postUserFuncInt
2462
     * Will execute a user function after the content has been created and each time it is fetched from Cache
2463
     * The result of this function itself will not be cached
2464
     *
2465
     * @param string $content Input value undergoing processing in this function.
2466
     * @param array $conf stdWrap properties for postUserFuncInt.
2467
     * @return string The processed input value
2468
     */
2469
    public function stdWrap_postUserFuncInt($content = '', $conf = [])
2470
    {
2471
        $substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash();
2472
        $this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [
2473
            'content' => $content,
2474
            'postUserFunc' => $conf['postUserFuncInt'],
2475
            'conf' => $conf['postUserFuncInt.'],
2476
            'type' => 'POSTUSERFUNC',
2477
            'cObj' => serialize($this)
2478
        ];
2479
        $content = '<!--' . $substKey . '-->';
2480
        return $content;
2481
    }
2482
2483
    /**
2484
     * prefixComment
2485
     * Will add HTML comments to the content to make it easier to identify certain content elements within the HTML output later on
2486
     *
2487
     * @param string $content Input value undergoing processing in this function.
2488
     * @param array $conf stdWrap properties for prefixComment.
2489
     * @return string The processed input value
2490
     */
2491
    public function stdWrap_prefixComment($content = '', $conf = [])
2492
    {
2493
        if (
2494
            (!isset($this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) || !$this->getTypoScriptFrontendController()->config['config']['disablePrefixComment'])
2495
            && !empty($conf['prefixComment'])
2496
        ) {
2497
            $content = $this->prefixComment($conf['prefixComment'], [], $content);
2498
        }
2499
        return $content;
2500
    }
2501
2502
    /**
2503
     * editIcons
2504
     * Will render icons for frontend editing as long as there is a BE user logged in
2505
     *
2506
     * @param string $content Input value undergoing processing in this function.
2507
     * @param array $conf stdWrap properties for editIcons.
2508
     * @return string The processed input value
2509
     */
2510
    public function stdWrap_editIcons($content = '', $conf = [])
2511
    {
2512
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn() && $conf['editIcons']) {
2513
            if (!isset($conf['editIcons.']) || !is_array($conf['editIcons.'])) {
2514
                $conf['editIcons.'] = [];
2515
            }
2516
            $content = $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']);
2517
        }
2518
        return $content;
2519
    }
2520
2521
    /**
2522
     * editPanel
2523
     * Will render the edit panel for frontend editing as long as there is a BE user logged in
2524
     *
2525
     * @param string $content Input value undergoing processing in this function.
2526
     * @param array $conf stdWrap properties for editPanel.
2527
     * @return string The processed input value
2528
     */
2529
    public function stdWrap_editPanel($content = '', $conf = [])
2530
    {
2531
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
2532
            $content = $this->editPanel($content, $conf['editPanel.']);
2533
        }
2534
        return $content;
2535
    }
2536
2537
    /**
2538
     * Store content into cache
2539
     *
2540
     * @param string $content Input value undergoing processing in these functions.
2541
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2542
     * @return string The processed input value
2543
     */
2544
    public function stdWrap_cacheStore($content = '', $conf = [])
2545
    {
2546
        if (!isset($conf['cache.'])) {
2547
            return $content;
2548
        }
2549
        $key = $this->calculateCacheKey($conf['cache.']);
2550
        if (empty($key)) {
2551
            return $content;
2552
        }
2553
        /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
2554
        $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
2555
        $tags = $this->calculateCacheTags($conf['cache.']);
2556
        $lifetime = $this->calculateCacheLifetime($conf['cache.']);
2557
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) {
2558
            $params = [
2559
                'key' => $key,
2560
                'content' => $content,
2561
                'lifetime' => $lifetime,
2562
                'tags' => $tags
2563
            ];
2564
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2565
            GeneralUtility::callUserFunction($_funcRef, $params, $ref);
2566
        }
2567
        $cacheFrontend->set($key, $content, $tags, $lifetime);
2568
        return $content;
2569
    }
2570
2571
    /**
2572
     * stdWrap post process hook
2573
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2574
     * this hook executes functions at after the content has been modified by the rest of the stdWrap functions but still before debugging
2575
     *
2576
     * @param string $content Input value undergoing processing in these functions.
2577
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2578
     * @return string The processed input value
2579
     */
2580
    public function stdWrap_stdWrapPostProcess($content = '', $conf = [])
2581
    {
2582
        foreach ($this->stdWrapHookObjects as $hookObject) {
2583
            /** @var ContentObjectStdWrapHookInterface $hookObject */
2584
            $content = $hookObject->stdWrapPostProcess($content, $conf, $this);
2585
        }
2586
        return $content;
2587
    }
2588
2589
    /**
2590
     * debug
2591
     * Will output the content as readable HTML code
2592
     *
2593
     * @param string $content Input value undergoing processing in this function.
2594
     * @return string The processed input value
2595
     */
2596
    public function stdWrap_debug($content = '')
2597
    {
2598
        return '<pre>' . htmlspecialchars($content) . '</pre>';
2599
    }
2600
2601
    /**
2602
     * debugFunc
2603
     * Will output the content in a debug table
2604
     *
2605
     * @param string $content Input value undergoing processing in this function.
2606
     * @param array $conf stdWrap properties for debugFunc.
2607
     * @return string The processed input value
2608
     */
2609
    public function stdWrap_debugFunc($content = '', $conf = [])
2610
    {
2611
        debug((int)$conf['debugFunc'] === 2 ? [$content] : $content);
2612
        return $content;
2613
    }
2614
2615
    /**
2616
     * debugData
2617
     * Will output the data used by the current record in a debug table
2618
     *
2619
     * @param string $content Input value undergoing processing in this function.
2620
     * @return string The processed input value
2621
     */
2622
    public function stdWrap_debugData($content = '')
2623
    {
2624
        debug($this->data, '$cObj->data:');
2625
        if (is_array($this->alternativeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->alternativeData) is always false.
Loading history...
2626
            debug($this->alternativeData, '$this->alternativeData');
2627
        }
2628
        return $content;
2629
    }
2630
2631
    /**
2632
     * Returns number of rows selected by the query made by the properties set.
2633
     * Implements the stdWrap "numRows" property
2634
     *
2635
     * @param array $conf TypoScript properties for the property (see link to "numRows")
2636
     * @return int The number of rows found by the select
2637
     * @internal
2638
     * @see stdWrap()
2639
     */
2640
    public function numRows($conf)
2641
    {
2642
        $conf['select.']['selectFields'] = 'count(*)';
2643
        $statement = $this->exec_getQuery($conf['table'], $conf['select.']);
2644
2645
        return (int)$statement->fetchOne();
2646
    }
2647
2648
    /**
2649
     * Exploding a string by the $char value (if integer its an ASCII value) and returning index $listNum
2650
     *
2651
     * @param string $content String to explode
2652
     * @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())
2653
     * @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.
2654
     * @return string
2655
     */
2656
    public function listNum($content, $listNum, $char)
2657
    {
2658
        $char = $char ?: ',';
2659
        if (MathUtility::canBeInterpretedAsInteger($char)) {
2660
            $char = chr((int)$char);
2661
        }
2662
        $temp = explode($char, $content);
2663
        if (empty($temp)) {
2664
            return '';
2665
        }
2666
        $last = '' . (count($temp) - 1);
2667
        // Take a random item if requested
2668
        if ($listNum === 'rand') {
2669
            $listNum = (string)random_int(0, count($temp) - 1);
2670
        }
2671
        $index = $this->calc(str_ireplace('last', $last, $listNum));
0 ignored issues
show
Bug introduced by
It seems like str_ireplace('last', $last, $listNum) can also be of type array; however, parameter $val of TYPO3\CMS\Frontend\Conte...tObjectRenderer::calc() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2671
        $index = $this->calc(/** @scrutinizer ignore-type */ str_ireplace('last', $last, $listNum));
Loading history...
2672
        return $temp[$index];
2673
    }
2674
2675
    /**
2676
     * Compares values together based on the settings in the input TypoScript array and returns the comparison result.
2677
     * Implements the "if" function in TYPO3 TypoScript
2678
     *
2679
     * @param array $conf TypoScript properties defining what to compare
2680
     * @return bool
2681
     * @see stdWrap()
2682
     * @see _parseFunc()
2683
     */
2684
    public function checkIf($conf)
2685
    {
2686
        if (!is_array($conf)) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
2687
            return true;
2688
        }
2689
        if (isset($conf['directReturn'])) {
2690
            return (bool)$conf['directReturn'];
2691
        }
2692
        $flag = true;
2693
        if (isset($conf['isNull.'])) {
2694
            $isNull = $this->stdWrap('', $conf['isNull.']);
2695
            if ($isNull !== null) {
0 ignored issues
show
introduced by
The condition $isNull !== null is always true.
Loading history...
2696
                $flag = false;
2697
            }
2698
        }
2699
        if (isset($conf['isTrue']) || isset($conf['isTrue.'])) {
2700
            $isTrue = trim((string)$this->stdWrapValue('isTrue', $conf ?? []));
2701
            if (!$isTrue) {
2702
                $flag = false;
2703
            }
2704
        }
2705
        if (isset($conf['isFalse']) || isset($conf['isFalse.'])) {
2706
            $isFalse = trim((string)$this->stdWrapValue('isFalse', $conf ?? []));
2707
            if ($isFalse) {
2708
                $flag = false;
2709
            }
2710
        }
2711
        if (isset($conf['isPositive']) || isset($conf['isPositive.'])) {
2712
            $number = $this->calc((string)$this->stdWrapValue('isPositive', $conf ?? []));
2713
            if ($number < 1) {
2714
                $flag = false;
2715
            }
2716
        }
2717
        if ($flag) {
2718
            $value = trim((string)$this->stdWrapValue('value', $conf ?? []));
2719
            if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) {
2720
                $number = trim((string)$this->stdWrapValue('isGreaterThan', $conf ?? []));
2721
                if ($number <= $value) {
2722
                    $flag = false;
2723
                }
2724
            }
2725
            if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) {
2726
                $number = trim((string)$this->stdWrapValue('isLessThan', $conf ?? []));
2727
                if ($number >= $value) {
2728
                    $flag = false;
2729
                }
2730
            }
2731
            if (isset($conf['equals']) || isset($conf['equals.'])) {
2732
                $number = trim((string)$this->stdWrapValue('equals', $conf ?? []));
2733
                if ($number != $value) {
2734
                    $flag = false;
2735
                }
2736
            }
2737
            if (isset($conf['isInList']) || isset($conf['isInList.'])) {
2738
                $number = trim((string)$this->stdWrapValue('isInList', $conf ?? []));
2739
                if (!GeneralUtility::inList($value, $number)) {
2740
                    $flag = false;
2741
                }
2742
            }
2743
            if (isset($conf['bitAnd']) || isset($conf['bitAnd.'])) {
2744
                $number = (int)trim((string)$this->stdWrapValue('bitAnd', $conf ?? []));
2745
                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

2745
                if ((new BitSet($number))->get(/** @scrutinizer ignore-type */ $value) === false) {
Loading history...
2746
                    $flag = false;
2747
                }
2748
            }
2749
        }
2750
        if ($conf['negate'] ?? false) {
2751
            $flag = !$flag;
2752
        }
2753
        return $flag;
2754
    }
2755
2756
    /**
2757
     * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser"
2758
     * together with the TypoScript options which are first converted from a TS style array
2759
     * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class.
2760
     *
2761
     * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser
2762
     * @param array $conf TypoScript properties for the parser. See link.
2763
     * @return string Return value.
2764
     * @see stdWrap()
2765
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLparserConfig()
2766
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLcleaner()
2767
     */
2768
    public function HTMLparser_TSbridge($theValue, $conf)
2769
    {
2770
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
2771
        $htmlParserCfg = $htmlParser->HTMLparserConfig($conf);
2772
        return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]);
2773
    }
2774
2775
    /**
2776
     * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes.
2777
     *
2778
     * @param string $content Input string being wrapped
2779
     * @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.
2780
     * @return string Output string wrapped in the wrapping value.
2781
     * @see insertData()
2782
     * @see stdWrap()
2783
     */
2784
    public function dataWrap($content, $wrap)
2785
    {
2786
        return $this->wrap($content, $this->insertData($wrap));
2787
    }
2788
2789
    /**
2790
     * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they
2791
     * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces.
2792
     * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine
2793
     * DBAL and is skipped here for later processing.
2794
     *
2795
     * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with
2796
     * the current pages title field value.
2797
     *
2798
     * @param string $str Input value
2799
     * @return string Processed input value
2800
     * @see getData()
2801
     * @see stdWrap()
2802
     * @see dataWrap()
2803
     */
2804
    public function insertData($str)
2805
    {
2806
        $inside = 0;
2807
        $newVal = '';
2808
        $pointer = 0;
2809
        $totalLen = strlen($str);
2810
        do {
2811
            if (!$inside) {
2812
                $len = strcspn(substr($str, $pointer), '{');
2813
                $newVal .= substr($str, $pointer, $len);
2814
                $inside = true;
2815
                if (substr($str, $pointer + $len + 1, 1) === '#') {
2816
                    $len2 = strcspn(substr($str, $pointer + $len), '}');
2817
                    $newVal .= substr($str, $pointer + $len, $len2);
2818
                    $len += $len2;
2819
                    $inside = false;
2820
                }
2821
            } else {
2822
                $len = strcspn(substr($str, $pointer), '}') + 1;
2823
                $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data);
2824
                $inside = false;
2825
            }
2826
            $pointer += $len;
2827
        } while ($pointer < $totalLen);
2828
        return $newVal;
2829
    }
2830
2831
    /**
2832
     * 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.
2833
     * Notice; this function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript.
2834
     *
2835
     * @param string $str Input value
2836
     * @param array $conf TypoScript Configuration (not used at this point.)
2837
     * @param string $content The content to wrap the comment around.
2838
     * @return string Processed input value
2839
     * @see stdWrap()
2840
     */
2841
    public function prefixComment($str, $conf, $content)
2842
    {
2843
        if (empty($str)) {
2844
            return $content;
2845
        }
2846
        $parts = explode('|', $str);
2847
        $indent = (int)$parts[0];
2848
        $comment = htmlspecialchars($this->insertData($parts[1]));
2849
        $output = LF
2850
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF
2851
            . str_pad('', $indent + 1, "\t") . $content . LF
2852
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF
2853
            . str_pad('', $indent + 1, "\t");
2854
        return $output;
2855
    }
2856
2857
    /**
2858
     * Implements the stdWrap property "substring" which is basically a TypoScript implementation of the PHP function, substr()
2859
     *
2860
     * @param string $content The string to perform the operation on
2861
     * @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().
2862
     * @return string The processed input value.
2863
     * @internal
2864
     * @see stdWrap()
2865
     */
2866
    public function substring($content, $options)
2867
    {
2868
        $options = GeneralUtility::intExplode(',', $options . ',');
2869
        if ($options[1]) {
2870
            return mb_substr($content, $options[0], $options[1], 'utf-8');
2871
        }
2872
        return mb_substr($content, $options[0], null, 'utf-8');
2873
    }
2874
2875
    /**
2876
     * 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.
2877
     *
2878
     * @param string $content The string to perform the operation on
2879
     * @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.
2880
     * @return string The processed input value.
2881
     * @internal
2882
     * @see stdWrap()
2883
     */
2884
    public function crop($content, $options)
2885
    {
2886
        $options = explode('|', $options);
2887
        $chars = (int)$options[0];
2888
        $afterstring = trim($options[1] ?? '');
2889
        $crop2space = trim($options[2] ?? '');
2890
        if ($chars) {
2891
            if (mb_strlen($content, 'utf-8') > abs($chars)) {
2892
                $truncatePosition = false;
2893
                if ($chars < 0) {
2894
                    $content = mb_substr($content, $chars, null, 'utf-8');
2895
                    if ($crop2space) {
2896
                        $truncatePosition = strpos($content, ' ');
2897
                    }
2898
                    $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content;
2899
                } else {
2900
                    $content = mb_substr($content, 0, $chars, 'utf-8');
2901
                    if ($crop2space) {
2902
                        $truncatePosition = strrpos($content, ' ');
2903
                    }
2904
                    $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring;
2905
                }
2906
            }
2907
        }
2908
        return $content;
2909
    }
2910
2911
    /**
2912
     * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length
2913
     * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string
2914
     * really was cropped.
2915
     *
2916
     * Compared to stdWrap.crop it respects HTML tags and entities.
2917
     *
2918
     * @param string $content The string to perform the operation on
2919
     * @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.
2920
     * @return string The processed input value.
2921
     * @internal
2922
     * @see stdWrap()
2923
     */
2924
    public function cropHTML($content, $options)
2925
    {
2926
        $options = explode('|', $options);
2927
        $chars = (int)$options[0];
2928
        $absChars = abs($chars);
2929
        $replacementForEllipsis = trim($options[1] ?? '');
2930
        $crop2space = trim($options[2] ?? '') === '1';
2931
        // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
2932
        $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';
2933
        $tagsRegEx = '
2934
			(
2935
				(?:
2936
					<!--.*?-->					# a comment
2937
					|
2938
					<canvas[^>]*>.*?</canvas>   # a canvas tag
2939
					|
2940
					<script[^>]*>.*?</script>   # a script tag
2941
					|
2942
					<noscript[^>]*>.*?</noscript> # a noscript tag
2943
					|
2944
					<template[^>]*>.*?</template> # a template tag
2945
				)
2946
				|
2947
				</?(?:' . $tags . ')+			# opening tag (\'<tag\') or closing tag (\'</tag\')
2948
				(?:
2949
					(?:
2950
						(?:
2951
							\\s+\\w[\\w-]*		# EITHER spaces, followed by attribute names
2952
							(?:
2953
								\\s*=?\\s*		# equals
2954
								(?>
2955
									".*?"		# attribute values in double-quotes
2956
									|
2957
									\'.*?\'		# attribute values in single-quotes
2958
									|
2959
									[^\'">\\s]+	# plain attribute values
2960
								)
2961
							)?
2962
						)
2963
						|						# OR a single dash (for TYPO3 link tag)
2964
						(?:
2965
							\\s+-
2966
						)
2967
					)+\\s*
2968
					|							# OR only spaces
2969
					\\s*
2970
				)
2971
				/?>								# closing the tag with \'>\' or \'/>\'
2972
			)';
2973
        $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
2974
        if ($splittedContent === false) {
2975
            $this->logger->debug('Unable to split "{content}" into tags.', ['content' => $content]);
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

Loading history...
2976
            $splittedContent = [];
2977
        }
2978
        // Reverse array if we are cropping from right.
2979
        if ($chars < 0) {
2980
            $splittedContent = array_reverse($splittedContent);
2981
        }
2982
        // Crop the text (chars of tag-blocks are not counted).
2983
        $strLen = 0;
2984
        // This is the offset of the content item which was cropped.
2985
        $croppedOffset = null;
2986
        $countSplittedContent = count($splittedContent);
2987
        for ($offset = 0; $offset < $countSplittedContent; $offset++) {
2988
            if ($offset % 2 === 0) {
2989
                $tempContent = $splittedContent[$offset];
2990
                $thisStrLen = mb_strlen(html_entity_decode($tempContent, ENT_COMPAT, 'UTF-8'), 'utf-8');
2991
                if ($strLen + $thisStrLen > $absChars) {
2992
                    $croppedOffset = $offset;
2993
                    $cropPosition = $absChars - $strLen;
2994
                    // The snippet "&[^&\s;]{2,8};" in the RegEx below represents entities.
2995
                    $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)';
2996
                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}#uis';
2997
                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
2998
                        $tempContentPlusOneCharacter = $croppedMatch[0];
2999
                    } else {
3000
                        $tempContentPlusOneCharacter = false;
3001
                    }
3002
                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}#uis';
3003
                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3004
                        $tempContent = $croppedMatch[0];
3005
                        if ($crop2space && $tempContentPlusOneCharacter !== false) {
3006
                            $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}(?=\\s)#uis';
3007
                            if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) {
3008
                                $tempContent = $croppedMatch[0];
3009
                            }
3010
                        }
3011
                    }
3012
                    $splittedContent[$offset] = $tempContent;
3013
                    break;
3014
                }
3015
                $strLen += $thisStrLen;
3016
            }
3017
        }
3018
        // Close cropped tags.
3019
        $closingTags = [];
3020
        if ($croppedOffset !== null) {
3021
            $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
3022
            $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
3023
            for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
3024
                if (substr($splittedContent[$offset], -2) === '/>') {
3025
                    // Ignore empty element tags (e.g. <br />).
3026
                    continue;
3027
                }
3028
                preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches);
3029
                $tagName = $matches[1] ?? null;
3030
                if ($tagName !== null) {
3031
                    // Seek for the closing (or opening) tag.
3032
                    $countSplittedContent = count($splittedContent);
3033
                    for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) {
3034
                        preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches);
3035
                        $seekingTagName = $matches[1] ?? null;
3036
                        if ($tagName === $seekingTagName) {
3037
                            // We found a matching tag.
3038
                            // Add closing tag only if it occurs after the cropped content item.
3039
                            if ($seekingOffset > $croppedOffset) {
3040
                                $closingTags[] = $splittedContent[$seekingOffset];
3041
                            }
3042
                            break;
3043
                        }
3044
                    }
3045
                }
3046
            }
3047
            // Drop the cropped items of the content array. The $closingTags will be added later on again.
3048
            array_splice($splittedContent, $croppedOffset + 1);
3049
        }
3050
        $splittedContent = array_merge($splittedContent, [
3051
            $croppedOffset !== null ? $replacementForEllipsis : ''
3052
        ], $closingTags);
3053
        // Reverse array once again if we are cropping from the end.
3054
        if ($chars < 0) {
3055
            $splittedContent = array_reverse($splittedContent);
3056
        }
3057
        return implode('', $splittedContent);
3058
    }
3059
3060
    /**
3061
     * 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())
3062
     *
3063
     * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
3064
     * @return int The result (might be a float if you did a division of the numbers).
3065
     * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()
3066
     */
3067
    public function calc($val)
3068
    {
3069
        $parts = GeneralUtility::splitCalc($val, '+-*/');
3070
        $value = 0;
3071
        foreach ($parts as $part) {
3072
            $theVal = $part[1];
3073
            $sign = $part[0];
3074
            if ((string)(int)$theVal === (string)$theVal) {
3075
                $theVal = (int)$theVal;
3076
            } else {
3077
                $theVal = 0;
3078
            }
3079
            if ($sign === '-') {
3080
                $value -= $theVal;
3081
            }
3082
            if ($sign === '+') {
3083
                $value += $theVal;
3084
            }
3085
            if ($sign === '/') {
3086
                if ((int)$theVal) {
3087
                    $value /= (int)$theVal;
3088
                }
3089
            }
3090
            if ($sign === '*') {
3091
                $value *= $theVal;
3092
            }
3093
        }
3094
        return $value;
3095
    }
3096
3097
    /**
3098
     * 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.
3099
     * 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.
3100
     * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse.
3101
     *
3102
     * @param string $value The string value to explode by $conf[token] and process each part
3103
     * @param array $conf TypoScript properties for "split
3104
     * @return string Compiled result
3105
     * @internal
3106
     * @see stdWrap()
3107
     * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::processItemStates()
3108
     */
3109
    public function splitObj($value, $conf)
3110
    {
3111
        $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'], $conf['token.']) : $conf['token'];
3112
        if ($conf['token'] === '') {
3113
            return $value;
3114
        }
3115
        $valArr = explode($conf['token'], $value);
3116
3117
        // return value directly by returnKey. No further processing
3118
        if (!empty($valArr) && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))) {
3119
            $key = (int)$this->stdWrapValue('returnKey', $conf ?? []);
3120
            return $valArr[$key] ?? '';
3121
        }
3122
3123
        // return the amount of elements. No further processing
3124
        if (!empty($valArr) && (($conf['returnCount'] ?? false) || ($conf['returnCount.'] ?? false))) {
3125
            $returnCount = (bool)$this->stdWrapValue('returnCount', $conf ?? []);
3126
            return $returnCount ? count($valArr) : 0;
3127
        }
3128
3129
        // calculate splitCount
3130
        $splitCount = count($valArr);
3131
        $max = (int)$this->stdWrapValue('max', $conf ?? []);
3132
        if ($max && $splitCount > $max) {
3133
            $splitCount = $max;
3134
        }
3135
        $min = (int)$this->stdWrapValue('min', $conf ?? []);
3136
        if ($min && $splitCount < $min) {
3137
            $splitCount = $min;
3138
        }
3139
        $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3140
        $cObjNumSplitConf = isset($conf['cObjNum.']) ? (string)$this->stdWrap($conf['cObjNum'], $conf['cObjNum.']) : (string)$conf['cObjNum'];
3141
        $splitArr = [];
3142
        if ($wrap !== '' || $cObjNumSplitConf !== '') {
3143
            $splitArr['wrap'] = $wrap;
3144
            $splitArr['cObjNum'] = $cObjNumSplitConf;
3145
            $splitArr = GeneralUtility::makeInstance(TypoScriptService::class)
3146
                ->explodeConfigurationForOptionSplit($splitArr, $splitCount);
3147
        }
3148
        $content = '';
3149
        for ($a = 0; $a < $splitCount; $a++) {
3150
            $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a;
3151
            $value = '' . $valArr[$a];
3152
            $this->data[$this->currentValKey] = $value;
3153
            if ($splitArr[$a]['cObjNum']) {
3154
                $objName = (int)$splitArr[$a]['cObjNum'];
3155
                $value = isset($conf[$objName . '.'])
3156
                    ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.'])
3157
                    : $this->cObjGet($conf[$objName . '.'], $objName . '.');
3158
            }
3159
            $wrap = (string)$this->stdWrapValue('wrap', $splitArr[$a] ?? []);
3160
            if ($wrap) {
3161
                $value = $this->wrap($value, $wrap);
3162
            }
3163
            $content .= $value;
3164
        }
3165
        return $content;
3166
    }
3167
3168
    /**
3169
     * Processes ordered replacements on content data.
3170
     *
3171
     * @param string $content The content to be processed
3172
     * @param array $configuration The TypoScript configuration for stdWrap.replacement
3173
     * @return string The processed content data
3174
     */
3175
    protected function replacement($content, array $configuration)
3176
    {
3177
        // Sorts actions in configuration by numeric index
3178
        ksort($configuration, SORT_NUMERIC);
3179
        foreach ($configuration as $index => $action) {
3180
            // Checks whether we have a valid action and a numeric key ending with a dot ("10.")
3181
            if (is_array($action) && substr($index, -1) === '.' && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) {
3182
                $content = $this->replacementSingle($content, $action);
3183
            }
3184
        }
3185
        return $content;
3186
    }
3187
3188
    /**
3189
     * Processes a single search/replace on content data.
3190
     *
3191
     * @param string $content The content to be processed
3192
     * @param array $configuration The TypoScript of the search/replace action to be processed
3193
     * @return string The processed content data
3194
     */
3195
    protected function replacementSingle($content, array $configuration)
3196
    {
3197
        if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) {
3198
            // Gets the strings
3199
            $search = (string)$this->stdWrapValue('search', $configuration ?? []);
3200
            $replace = (string)$this->stdWrapValue('replace', $configuration, null);
3201
3202
            // Determines whether regular expression shall be used
3203
            $useRegularExpression = (bool)$this->stdWrapValue('useRegExp', $configuration, false);
3204
3205
            // Determines whether replace-pattern uses option-split
3206
            $useOptionSplitReplace = (bool)$this->stdWrapValue('useOptionSplitReplace', $configuration, false);
3207
3208
            // Performs a replacement by preg_replace()
3209
            if ($useRegularExpression) {
3210
                // Get separator-character which precedes the string and separates search-string from the modifiers
3211
                $separator = $search[0];
3212
                $startModifiers = strrpos($search, $separator);
3213
                if ($separator !== false && $startModifiers > 0) {
3214
                    $modifiers = substr($search, $startModifiers + 1);
3215
                    // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code
3216
                    $modifiers = str_replace('e', '', $modifiers);
3217
                    $search = substr($search, 0, $startModifiers + 1) . $modifiers;
3218
                }
3219
                if ($useOptionSplitReplace) {
3220
                    // init for replacement
3221
                    $splitCount = preg_match_all($search, $content, $matches);
3222
                    $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3223
                    $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3224
                    $replaceCount = 0;
3225
3226
                    $replaceCallback = function ($match) use ($replaceArray, $search, &$replaceCount) {
3227
                        $replaceCount++;
3228
                        return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]);
3229
                    };
3230
                    $content = preg_replace_callback($search, $replaceCallback, $content);
3231
                } else {
3232
                    $content = preg_replace($search, $replace, $content);
3233
                }
3234
            } elseif ($useOptionSplitReplace) {
3235
                // turn search-string into a preg-pattern
3236
                $searchPreg = '#' . preg_quote($search, '#') . '#';
3237
3238
                // init for replacement
3239
                $splitCount = preg_match_all($searchPreg, $content, $matches);
3240
                $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3241
                $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3242
                $replaceCount = 0;
3243
3244
                $replaceCallback = function () use ($replaceArray, &$replaceCount) {
3245
                    $replaceCount++;
3246
                    return $replaceArray[$replaceCount - 1][0];
3247
                };
3248
                $content = preg_replace_callback($searchPreg, $replaceCallback, $content);
3249
            } else {
3250
                $content = str_replace($search, $replace, $content);
3251
            }
3252
        }
3253
        return $content;
3254
    }
3255
3256
    /**
3257
     * Implements the "round" property of stdWrap
3258
     * This is a Wrapper function for PHP's rounding functions (round,ceil,floor), defaults to round()
3259
     *
3260
     * @param string $content Value to process
3261
     * @param array $conf TypoScript configuration for round
3262
     * @return string The formatted number
3263
     */
3264
    protected function round($content, array $conf = [])
3265
    {
3266
        $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3267
        $type = $this->stdWrapValue('roundType', $conf ?? []);
3268
        $floatVal = (float)$content;
3269
        switch ($type) {
3270
            case 'ceil':
3271
                $content = ceil($floatVal);
3272
                break;
3273
            case 'floor':
3274
                $content = floor($floatVal);
3275
                break;
3276
            case 'round':
3277
3278
            default:
3279
                $content = round($floatVal, $decimals);
3280
        }
3281
        return $content;
3282
    }
3283
3284
    /**
3285
     * Implements the stdWrap property "numberFormat"
3286
     * This is a Wrapper function for php's number_format()
3287
     *
3288
     * @param float $content Value to process
3289
     * @param array $conf TypoScript Configuration for numberFormat
3290
     * @return string The formatted number
3291
     */
3292
    public function numberFormat($content, $conf)
3293
    {
3294
        $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3295
        $dec_point = (string)$this->stdWrapValue('dec_point', $conf, '.');
3296
        $thousands_sep = (string)$this->stdWrapValue('thousands_sep', $conf, ',');
3297
        return number_format((float)$content, $decimals, $dec_point, $thousands_sep);
3298
    }
3299
3300
    /**
3301
     * Implements the stdWrap property, "parseFunc".
3302
     * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text
3303
     * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc.
3304
     * It is still a very important function for processing of bodytext which is normally stored in the database
3305
     * in a format which is not fully ready to be outputted.
3306
     * This situation has not become better by having a RTE around...
3307
     *
3308
     * This function is actually just splitting the input content according to the configuration of "external blocks".
3309
     * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed
3310
     * (while other parts/blocks should NOT be parsed).
3311
     * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc()
3312
     *
3313
     * @param string $theValue The value to process.
3314
     * @param array $conf TypoScript configuration for parseFunc
3315
     * @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!
3316
     * @return string The processed value
3317
     * @see _parseFunc()
3318
     */
3319
    public function parseFunc($theValue, $conf, $ref = '')
3320
    {
3321
        // Fetch / merge reference, if any
3322
        if ($ref) {
3323
            $temp_conf = [
3324
                'parseFunc' => $ref,
3325
                'parseFunc.' => $conf
3326
            ];
3327
            $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc');
3328
            $conf = $temp_conf['parseFunc.'];
3329
        }
3330
        // Process:
3331
        if ((string)($conf['externalBlocks'] ?? '') === '') {
3332
            return $this->_parseFunc($theValue, $conf);
3333
        }
3334
        $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks'])));
3335
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3336
        $parts = $htmlParser->splitIntoBlock($tags, $theValue);
3337
        foreach ($parts as $k => $v) {
3338
            if ($k % 2) {
3339
                // font:
3340
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3341
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3342
                if ($cfg['stripNLprev'] || $cfg['stripNL']) {
3343
                    $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]);
3344
                }
3345
                if ($cfg['stripNLnext'] || $cfg['stripNL']) {
3346
                    $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]);
3347
                }
3348
            }
3349
        }
3350
        foreach ($parts as $k => $v) {
3351
            if ($k % 2) {
3352
                $tag = $htmlParser->getFirstTag($v);
3353
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3354
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3355
                if ($cfg['callRecursive']) {
3356
                    $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf);
3357
                    if (!$cfg['callRecursive.']['dontWrapSelf']) {
3358
                        if ($cfg['callRecursive.']['alternativeWrap']) {
3359
                            $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']);
3360
                        } else {
3361
                            if (is_array($cfg['callRecursive.']['tagStdWrap.'])) {
3362
                                $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']);
3363
                            }
3364
                            $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>';
3365
                        }
3366
                    }
3367
                } elseif ($cfg['HTMLtableCells']) {
3368
                    $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]);
3369
                    foreach ($rowParts as $kk => $vv) {
3370
                        if ($kk % 2) {
3371
                            $colParts = $htmlParser->splitIntoBlock('td,th', $vv);
3372
                            $cc = 0;
3373
                            foreach ($colParts as $kkk => $vvv) {
3374
                                if ($kkk % 2) {
3375
                                    $cc++;
3376
                                    $tag = $htmlParser->getFirstTag($vvv);
3377
                                    $tagName = strtolower($htmlParser->getFirstTagName($vvv));
3378
                                    $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv);
3379
                                    if ($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] || !isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && $cfg['HTMLtableCells.']['default.']['callRecursive']) {
3380
                                        if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs']) {
3381
                                            $colParts[$kkk] = str_replace('</p><p>', '</p>' . LF . '<p>', $colParts[$kkk]);
3382
                                        }
3383
                                        $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf);
3384
                                    }
3385
                                    $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'])
3386
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']
3387
                                        : $cfg['HTMLtableCells.']['default.']['tagStdWrap.'];
3388
                                    if (is_array($tagStdWrap)) {
3389
                                        $tag = $this->stdWrap($tag, $tagStdWrap);
3390
                                    }
3391
                                    $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'])
3392
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']
3393
                                        : $cfg['HTMLtableCells.']['default.']['stdWrap.'];
3394
                                    if (is_array($stdWrap)) {
3395
                                        $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap);
3396
                                    }
3397
                                    $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>';
3398
                                }
3399
                            }
3400
                            $rowParts[$kk] = implode('', $colParts);
3401
                        }
3402
                    }
3403
                    $parts[$k] = implode('', $rowParts);
3404
                }
3405
                if (is_array($cfg['stdWrap.'])) {
3406
                    $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
3407
                }
3408
            } else {
3409
                $parts[$k] = $this->_parseFunc($parts[$k], $conf);
3410
            }
3411
        }
3412
        return implode('', $parts);
3413
    }
3414
3415
    /**
3416
     * Helper function for parseFunc()
3417
     *
3418
     * @param string $theValue The value to process.
3419
     * @param array $conf TypoScript configuration for parseFunc
3420
     * @return string The processed value
3421
     * @internal
3422
     * @see parseFunc()
3423
     */
3424
    public function _parseFunc($theValue, $conf)
3425
    {
3426
        if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
3427
            return $theValue;
3428
        }
3429
        // Indicates that the data is from within a tag.
3430
        $inside = false;
3431
        // Pointer to the total string position
3432
        $pointer = 0;
3433
        // Loaded with the current typo-tag if any.
3434
        $currentTag = null;
3435
        $stripNL = 0;
3436
        $contentAccum = [];
3437
        $contentAccumP = 0;
3438
        $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? ''));
3439
        $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? ''));
3440
        $totalLen = strlen($theValue);
3441
        do {
3442
            if (!$inside) {
3443
                if ($currentTag === null) {
3444
                    // These operations should only be performed on code outside the typotags...
3445
                    // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/'
3446
                    $len_p = 0;
3447
                    $c = 100;
3448
                    do {
3449
                        $len = strcspn(substr($theValue, $pointer + $len_p), '<');
3450
                        $len_p += $len + 1;
3451
                        $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1)));
3452
                        $c--;
3453
                    } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47);
3454
                    $len = $len_p - 1;
3455
                } else {
3456
                    $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, (string)$currentTag[0]);
3457
                }
3458
                // $data is the content until the next <tag-start or end is detected.
3459
                // In case of a currentTag set, this would mean all data between the start- and end-tags
3460
                $data = substr($theValue, $pointer, $len);
3461
                if ($data !== false) {
3462
                    if ($stripNL) {
3463
                        // If the previous tag was set to strip NewLines in the beginning of the next data-chunk.
3464
                        $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data);
3465
                        if ($data === null) {
3466
                            $this->logger->debug('Stripping new lines failed for "{data}"', ['data' => $data]);
3467
                            $data = '';
3468
                        }
3469
                    }
3470
                    // These operations should only be performed on code outside the tags...
3471
                    if (!is_array($currentTag)) {
3472
                        // Constants
3473
                        $tsfe = $this->getTypoScriptFrontendController();
3474
                        $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null;
3475
                        if (!empty($conf['constants']) && is_array($tmpConstants)) {
3476
                            foreach ($tmpConstants as $key => $val) {
3477
                                if (is_string($val)) {
3478
                                    $data = str_replace('###' . $key . '###', $val, $data);
3479
                                }
3480
                            }
3481
                        }
3482
                        // Short
3483
                        if (isset($conf['short.']) && is_array($conf['short.'])) {
3484
                            $shortWords = $conf['short.'];
3485
                            krsort($shortWords);
3486
                            foreach ($shortWords as $key => $val) {
3487
                                if (is_string($val)) {
3488
                                    $data = str_replace($key, $val, $data);
3489
                                }
3490
                            }
3491
                        }
3492
                        // stdWrap
3493
                        if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) {
3494
                            $data = $this->stdWrap($data, $conf['plainTextStdWrap.']);
3495
                        }
3496
                        // userFunc
3497
                        if ($conf['userFunc'] ?? false) {
3498
                            $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $data);
3499
                        }
3500
                        // Makelinks: (Before search-words as we need the links to be generated when searchwords go on...!)
3501
                        if ($conf['makelinks'] ?? false) {
3502
                            $data = $this->http_makelinks($data, $conf['makelinks.']['http.']);
3503
                            $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []);
3504
                        }
3505
                        // Search Words:
3506
                        if (($tsfe->no_cache ?? false) && $conf['sword'] && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) {
3507
                            $newstring = '';
3508
                            do {
3509
                                $pregSplitMode = 'i';
3510
                                if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) {
3511
                                    $pregSplitMode = '';
3512
                                }
3513
                                $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2);
3514
                                $newstring .= $pieces[0];
3515
                                $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1]));
3516
                                $inTag = false;
3517
                                if (strpos($pieces[0], '<') !== false || strpos($pieces[0], '>') !== false) {
3518
                                    // Returns TRUE, if a '<' is closer to the string-end than '>'.
3519
                                    // This is the case if we're INSIDE a tag (that could have been
3520
                                    // made by makelinks...) and we must secure, that the inside of a tag is
3521
                                    // not marked up.
3522
                                    $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>');
3523
                                }
3524
                                // The searchword:
3525
                                $match = substr($data, strlen($pieces[0]), $match_len);
3526
                                if (trim($match) && strlen($match) > 1 && !$inTag) {
3527
                                    $match = $this->wrap($match, $conf['sword']);
3528
                                }
3529
                                // Concatenate the Search Word again.
3530
                                $newstring .= $match;
3531
                                $data = $pieces[1];
3532
                            } while ($pieces[1]);
3533
                            $data = $newstring;
3534
                        }
3535
                    }
3536
                    // Search for tags to process in current data and
3537
                    // call this method recursively if found
3538
                    if (strpos($data, '<') !== false && isset($conf['tags.']) && is_array($conf['tags.'])) {
3539
                        foreach ($conf['tags.'] as $tag => $tagConfig) {
3540
                            // only match tag `a` in `<a href"...">` but not in `<abbr>`
3541
                            if (preg_match('#<' . $tag . '[\s/>]#', $data)) {
3542
                                $data = $this->_parseFunc($data, $conf);
3543
                                break;
3544
                            }
3545
                        }
3546
                    }
3547
                    $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3548
                        ? $contentAccum[$contentAccumP] . $data
3549
                        : $data;
3550
                }
3551
                $inside = true;
3552
            } else {
3553
                // tags
3554
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
3555
                $data = substr($theValue, $pointer, $len);
3556
                if (StringUtility::endsWith($data, '/>') && strpos($data, '<link ') !== 0) {
3557
                    $tagContent = substr($data, 1, -2);
3558
                } else {
3559
                    $tagContent = substr($data, 1, -1);
3560
                }
3561
                $tag = explode(' ', trim($tagContent), 2);
3562
                $tag[0] = strtolower($tag[0]);
3563
                // end tag like </li>
3564
                if ($tag[0][0] === '/') {
3565
                    $tag[0] = substr($tag[0], 1);
3566
                    $tag['out'] = 1;
3567
                }
3568
                if ($conf['tags.'][$tag[0]] ?? false) {
3569
                    $treated = false;
3570
                    $stripNL = false;
3571
                    // in-tag
3572
                    if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) {
3573
                        // $currentTag (array!) is the tag we are currently processing
3574
                        $currentTag = $tag;
3575
                        $contentAccumP++;
3576
                        $treated = true;
3577
                        // in-out-tag: img and other empty tags
3578
                        if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', (string)$tag[0])) {
3579
                            $tag['out'] = 1;
3580
                        }
3581
                    }
3582
                    // out-tag
3583
                    if ($currentTag[0] === $tag[0] && isset($tag['out']) && $tag['out']) {
3584
                        $theName = $conf['tags.'][$tag[0]];
3585
                        $theConf = $conf['tags.'][$tag[0] . '.'];
3586
                        // This flag indicates, that NL- (13-10-chars) should be stripped first and last.
3587
                        $stripNL = (bool)($theConf['stripNL'] ?? false);
3588
                        // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content.
3589
                        $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false);
3590
                        $this->parameters = [];
3591
                        if (isset($currentTag[1])) {
3592
                            // decode HTML entities in attributes, since they're processed
3593
                            $params = GeneralUtility::get_tag_attributes((string)$currentTag[1], true);
3594
                            if (is_array($params)) {
3595
                                foreach ($params as $option => $val) {
3596
                                    // contains non-encoded values
3597
                                    $this->parameters[strtolower($option)] = $val;
3598
                                }
3599
                            }
3600
                            $this->parameters['allParams'] = trim((string)$currentTag[1]);
3601
                        }
3602
                        // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer.
3603
                        // $stripNL depends on the configuration of the current tag
3604
                        if ($stripNL) {
3605
                            $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1]);
3606
                            $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP]);
3607
                            $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP]);
3608
                        }
3609
                        $this->data[$this->currentValKey] = $contentAccum[$contentAccumP];
3610
                        $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]);
3611
                        // fetch the content object
3612
                        $contentAccum[$contentAccumP] = $newInput;
3613
                        $contentAccumP++;
3614
                        // If the TypoTag section
3615
                        if (!$breakOut) {
3616
                            if (!isset($contentAccum[$contentAccumP - 2])) {
3617
                                $contentAccum[$contentAccumP - 2] = '';
3618
                            }
3619
                            $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? '');
3620
                            unset($contentAccum[$contentAccumP]);
3621
                            unset($contentAccum[$contentAccumP - 1]);
3622
                            $contentAccumP -= 2;
3623
                        }
3624
                        $currentTag = null;
3625
                        $treated = true;
3626
                    }
3627
                    // other tags
3628
                    if (!$treated) {
3629
                        $contentAccum[$contentAccumP] .= $data;
3630
                    }
3631
                } else {
3632
                    // If a tag was not a typo tag, then it is just added to the content
3633
                    $stripNL = false;
3634
                    if (GeneralUtility::inList($allowTags, (string)$tag[0]) ||
3635
                        ($denyTags !== '*' && !GeneralUtility::inList($denyTags, (string)$tag[0]))) {
3636
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3637
                            ? $contentAccum[$contentAccumP] . $data
3638
                            : $data;
3639
                    } else {
3640
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3641
                            ? $contentAccum[$contentAccumP] . htmlspecialchars($data)
3642
                            : htmlspecialchars($data);
3643
                    }
3644
                }
3645
                $inside = false;
3646
            }
3647
            $pointer += $len;
3648
        } while ($pointer < $totalLen);
3649
        // Parsing nonTypoTag content (all even keys):
3650
        reset($contentAccum);
3651
        $contentAccumCount = count($contentAccum);
3652
        for ($a = 0; $a < $contentAccumCount; $a++) {
3653
            if ($a % 2 != 1) {
3654
                // stdWrap
3655
                if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) {
3656
                    $contentAccum[$a] = $this->stdWrap((string)($contentAccum[$a] ?? ''), $conf['nonTypoTagStdWrap.']);
3657
                }
3658
                // userFunc
3659
                if (!empty($conf['nonTypoTagUserFunc'])) {
3660
                    $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'] ?? [], (string)($contentAccum[$a] ?? ''));
3661
                }
3662
            }
3663
        }
3664
        return implode('', $contentAccum);
3665
    }
3666
3667
    /**
3668
     * Lets you split the content by LF and process each line independently. Used to format content made with the RTE.
3669
     *
3670
     * @param string $theValue The input value
3671
     * @param array $conf TypoScript options
3672
     * @return string The processed input value being returned; Splitted lines imploded by LF again.
3673
     * @internal
3674
     */
3675
    public function encaps_lineSplit($theValue, $conf)
3676
    {
3677
        if ((string)$theValue === '') {
3678
            return '';
3679
        }
3680
        $lParts = explode(LF, $theValue);
3681
3682
        // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line.
3683
        $lastPartIndex = count($lParts) - 1;
3684
        if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') {
3685
            array_pop($lParts);
3686
        }
3687
3688
        $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList']), true);
3689
        $nonWrappedTag = $conf['nonWrappedTag'];
3690
        $defaultAlign = trim((string)$this->stdWrapValue('defaultAlign', $conf ?? []));
3691
3692
        $str_content = '';
3693
        foreach ($lParts as $k => $l) {
3694
            $sameBeginEnd = 0;
3695
            $emptyTag = false;
3696
            $l = trim($l);
3697
            $attrib = [];
3698
            $nonWrapped = false;
3699
            $tagName = '';
3700
            if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') {
3701
                $fwParts = explode('>', substr($l, 1), 2);
3702
                [$tagName] = explode(' ', $fwParts[0], 2);
3703
                if (!$fwParts[1]) {
3704
                    if (substr($tagName, -1) === '/') {
3705
                        $tagName = substr($tagName, 0, -1);
3706
                    }
3707
                    if (substr($fwParts[0], -1) === '/') {
3708
                        $sameBeginEnd = 1;
3709
                        $emptyTag = true;
3710
                        // decode HTML entities, they're encoded later again
3711
                        $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true);
3712
                    }
3713
                } else {
3714
                    $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2);
3715
                    // decode HTML entities, they're encoded later again
3716
                    $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true);
3717
                    $str_content = $backParts[0];
3718
                    $sameBeginEnd = substr(strtolower($backParts[1]), 1, strlen($tagName)) === strtolower($tagName);
3719
                }
3720
            }
3721
            if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) {
3722
                $uTagName = strtoupper($tagName);
3723
                $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName);
3724
            } else {
3725
                $uTagName = strtoupper($nonWrappedTag);
3726
                // The line will be wrapped: $uTagName should not be an empty tag
3727
                $emptyTag = false;
3728
                $str_content = $lParts[$k];
3729
                $nonWrapped = true;
3730
                $attrib = [];
3731
            }
3732
            // Wrapping all inner-content:
3733
            if (is_array($conf['innerStdWrap_all.'])) {
3734
                $str_content = $this->stdWrap($str_content, $conf['innerStdWrap_all.']);
3735
            }
3736
            if ($uTagName) {
3737
                // Setting common attributes
3738
                if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) {
3739
                    foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) {
3740
                        if (!is_array($vv)) {
3741
                            if ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'blank') {
3742
                                if ((string)($attrib[$kk] ?? '') === '') {
3743
                                    $attrib[$kk] = $vv;
3744
                                }
3745
                            } elseif ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'exists') {
3746
                                if (!isset($attrib[$kk])) {
3747
                                    $attrib[$kk] = $vv;
3748
                                }
3749
                            } else {
3750
                                $attrib[$kk] = $vv;
3751
                            }
3752
                        }
3753
                    }
3754
                }
3755
                // Wrapping all inner-content:
3756
                if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) {
3757
                    $str_content = $this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']);
3758
                }
3759
                // Default align
3760
                if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) {
3761
                    $attrib['align'] = $defaultAlign;
3762
                }
3763
                // implode (insecure) attributes, that's why `htmlspecialchars` is used here
3764
                $params = GeneralUtility::implodeAttributes($attrib, true);
3765
                if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) {
3766
                    $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
3767
                    if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) {
3768
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />';
3769
                    } else {
3770
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>';
3771
                    }
3772
                }
3773
            }
3774
            if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) {
3775
                $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']);
3776
            }
3777
            $lParts[$k] = $str_content;
3778
        }
3779
        return implode(LF, $lParts);
3780
    }
3781
3782
    /**
3783
     * Finds URLS in text and makes it to a real link.
3784
     * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link,
3785
     * linking to the URL we should have found.
3786
     *
3787
     * @param string $data The string in which to search for "http://
3788
     * @param array $conf Configuration for makeLinks, see link
3789
     * @return string The processed input string, being returned.
3790
     * @see _parseFunc()
3791
     */
3792
    public function http_makelinks($data, $conf)
3793
    {
3794
        $parts = [];
3795
        $aTagParams = $this->getATagParams($conf);
3796
        foreach (['http://', 'https://'] as $scheme) {
3797
            $textpieces = explode($scheme, $data);
3798
            $pieces = count($textpieces);
3799
            $textstr = $textpieces[0];
3800
            for ($i = 1; $i < $pieces; $i++) {
3801
                $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3802
                if (trim(substr($textstr, -1)) === '' && $len) {
3803
                    $lastChar = substr($textpieces[$i], $len - 1, 1);
3804
                    if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) {
3805
                        $len--;
3806
                    }
3807
                    // Included '\/' 3/12
3808
                    $parts[0] = substr($textpieces[$i], 0, $len);
3809
                    $parts[1] = substr($textpieces[$i], $len);
3810
                    $keep = $conf['keep'];
3811
                    $linkParts = parse_url($scheme . $parts[0]);
3812
                    $linktxt = '';
3813
                    if (strpos($keep, 'scheme') !== false) {
3814
                        $linktxt = $scheme;
3815
                    }
3816
                    $linktxt .= $linkParts['host'];
3817
                    if (strpos($keep, 'path') !== false) {
3818
                        $linktxt .= $linkParts['path'];
3819
                        // Added $linkParts['query'] 3/12
3820
                        if (strpos($keep, 'query') !== false && $linkParts['query']) {
3821
                            $linktxt .= '?' . $linkParts['query'];
3822
                        } elseif ($linkParts['path'] === '/') {
3823
                            $linktxt = substr($linktxt, 0, -1);
3824
                        }
3825
                    }
3826
                    $target = (string)$this->stdWrapValue('extTarget', $conf, $this->getTypoScriptFrontendController()->extTarget);
3827
3828
                    // check for jump URLs or similar
3829
                    $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf) ?? '';
3830
3831
                    $res = '<a href="' . htmlspecialchars($linkUrl) . '"'
3832
                        . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
3833
                        . $aTagParams . '>';
3834
3835
                    $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3836
                    if ((string)$conf['ATagBeforeWrap'] !== '') {
3837
                        $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
3838
                    } else {
3839
                        $res = $this->wrap($res . $linktxt . '</a>', $wrap);
3840
                    }
3841
                    $textstr .= $res . $parts[1];
3842
                } else {
3843
                    $textstr .= $scheme . $textpieces[$i];
3844
                }
3845
            }
3846
            $data = $textstr;
3847
        }
3848
        return $textstr;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $textstr seems to be defined by a foreach iteration on line 3796. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
3849
    }
3850
3851
    /**
3852
     * Will find all strings prefixed with "mailto:" in the $data string and make them into a link,
3853
     * linking to the email address they point to.
3854
     *
3855
     * @param string $data The string in which to search for "mailto:
3856
     * @param array $conf Configuration for makeLinks, see link
3857
     * @return string The processed input string, being returned.
3858
     * @see _parseFunc()
3859
     */
3860
    public function mailto_makelinks($data, $conf)
3861
    {
3862
        $conf = (array)$conf;
3863
        $parts = [];
3864
        // http-split
3865
        $aTagParams = $this->getATagParams($conf);
3866
        $textpieces = explode('mailto:', $data);
3867
        $pieces = count($textpieces);
3868
        $textstr = $textpieces[0];
3869
        $tsfe = $this->getTypoScriptFrontendController();
3870
        for ($i = 1; $i < $pieces; $i++) {
3871
            $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3872
            if (trim(substr($textstr, -1)) === '' && $len) {
3873
                $lastChar = substr($textpieces[$i], $len - 1, 1);
3874
                if (!preg_match('/[A-Za-z0-9]/', $lastChar)) {
3875
                    $len--;
3876
                }
3877
                $parts[0] = substr($textpieces[$i], 0, $len);
3878
                $parts[1] = substr($textpieces[$i], $len);
3879
                $linktxt = (string)preg_replace('/\\?.*/', '', $parts[0]);
3880
                [$mailToUrl, $linktxt] = $this->getMailTo($parts[0], $linktxt);
3881
                $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl);
3882
                $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>';
3883
                $wrap = (string)$this->stdWrapValue('wrap', $conf);
3884
                if ((string)$conf['ATagBeforeWrap'] !== '') {
3885
                    $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
0 ignored issues
show
Bug introduced by
It seems like $linktxt can also be of type array; however, parameter $content of TYPO3\CMS\Frontend\Conte...tObjectRenderer::wrap() 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

3885
                    $res = $res . $this->wrap(/** @scrutinizer ignore-type */ $linktxt, $wrap) . '</a>';
Loading history...
3886
                } else {
3887
                    $res = $this->wrap($res . $linktxt . '</a>', $wrap);
0 ignored issues
show
Bug introduced by
Are you sure $linktxt of type array|string 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

3887
                    $res = $this->wrap($res . /** @scrutinizer ignore-type */ $linktxt . '</a>', $wrap);
Loading history...
3888
                }
3889
                $textstr .= $res . $parts[1];
3890
            } else {
3891
                $textstr .= 'mailto:' . $textpieces[$i];
3892
            }
3893
        }
3894
        return $textstr;
3895
    }
3896
3897
    /**
3898
     * Creates and returns a TypoScript "imgResource".
3899
     * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER".
3900
     * In the first case a current image is returned, possibly scaled down or otherwise processed.
3901
     * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines.
3902
     * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']);
3903
     *
3904
     * Structure of the returned info array:
3905
     *  0 => width
3906
     *  1 => height
3907
     *  2 => file extension
3908
     *  3 => file name
3909
     *  origFile => original file name
3910
     *  origFile_mtime => original file mtime
3911
     *  -- only available if processed via FAL: --
3912
     *  originalFile => original file object
3913
     *  processedFile => processed file object
3914
     *  fileCacheHash => checksum of processed file
3915
     *
3916
     * @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.
3917
     * @param array $fileArray TypoScript properties for the imgResource type
3918
     * @return array|null Returns info-array
3919
     * @see cImage()
3920
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder
3921
     */
3922
    public function getImgResource($file, $fileArray)
3923
    {
3924
        $importedFile = null;
3925
        if (empty($file) && empty($fileArray)) {
3926
            return null;
3927
        }
3928
        if (!is_array($fileArray)) {
0 ignored issues
show
introduced by
The condition is_array($fileArray) is always true.
Loading history...
3929
            $fileArray = (array)$fileArray;
3930
        }
3931
        $imageResource = null;
3932
        if ($file === 'GIFBUILDER') {
3933
            $gifCreator = GeneralUtility::makeInstance(GifBuilder::class);
3934
            $theImage = '';
3935
            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) {
3936
                $gifCreator->start($fileArray, $this->data);
3937
                $theImage = $gifCreator->gifBuild();
3938
            }
3939
            $imageResource = $gifCreator->getImageDimensions($theImage);
3940
            $imageResource['origFile'] = $theImage;
3941
        } else {
3942
            if ($file instanceof File) {
3943
                $fileObject = $file;
3944
            } elseif ($file instanceof FileReference) {
3945
                $fileObject = $file->getOriginalFile();
3946
            } else {
3947
                try {
3948
                    if (isset($fileArray['import.']) && $fileArray['import.']) {
3949
                        $importedFile = trim($this->stdWrap('', $fileArray['import.']));
3950
                        if (!empty($importedFile)) {
3951
                            $file = $importedFile;
3952
                        }
3953
                    }
3954
3955
                    if (MathUtility::canBeInterpretedAsInteger($file)) {
3956
                        $treatIdAsReference = $this->stdWrapValue('treatIdAsReference', $fileArray ?? []);
3957
                        if (!empty($treatIdAsReference)) {
3958
                            $file = $this->getResourceFactory()->getFileReferenceObject($file);
3959
                            $fileObject = $file->getOriginalFile();
3960
                        } else {
3961
                            $fileObject = $this->getResourceFactory()->getFileObject($file);
3962
                        }
3963
                    } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier
3964
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
3965
                    } else {
3966
                        if (isset($importedFile) && !empty($importedFile) && !empty($fileArray['import'])) {
3967
                            $file = $fileArray['import'] . $file;
3968
                        }
3969
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
3970
                    }
3971
                } catch (Exception $exception) {
3972
                    $this->logger->warning('The image "{file}" could not be found and won\'t be included in frontend output', [
3973
                        'file' => $file,
3974
                        'exception' => $exception,
3975
                    ]);
3976
                    return null;
3977
                }
3978
            }
3979
            if ($fileObject instanceof File) {
3980
                $processingConfiguration = [];
3981
                $processingConfiguration['width'] = $this->stdWrapValue('width', $fileArray ?? []);
3982
                $processingConfiguration['height'] = $this->stdWrapValue('height', $fileArray ?? []);
3983
                $processingConfiguration['fileExtension'] = $this->stdWrapValue('ext', $fileArray ?? []);
3984
                $processingConfiguration['maxWidth'] = (int)$this->stdWrapValue('maxW', $fileArray ?? []);
3985
                $processingConfiguration['maxHeight'] = (int)$this->stdWrapValue('maxH', $fileArray ?? []);
3986
                $processingConfiguration['minWidth'] = (int)$this->stdWrapValue('minW', $fileArray ?? []);
3987
                $processingConfiguration['minHeight'] = (int)$this->stdWrapValue('minH', $fileArray ?? []);
3988
                $processingConfiguration['noScale'] = $this->stdWrapValue('noScale', $fileArray ?? []);
3989
                $processingConfiguration['additionalParameters'] = $this->stdWrapValue('params', $fileArray ?? []);
3990
                $processingConfiguration['frame'] = (int)$this->stdWrapValue('frame', $fileArray ?? []);
3991
                if ($file instanceof FileReference) {
3992
                    $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($file, $fileArray);
3993
                } else {
3994
                    $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray);
3995
                }
3996
3997
                // Possibility to cancel/force profile extraction
3998
                // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']
3999
                if (isset($fileArray['stripProfile'])) {
4000
                    $processingConfiguration['stripProfile'] = $fileArray['stripProfile'];
4001
                }
4002
                // Check if we can handle this type of file for editing
4003
                if ($fileObject->isImage()) {
4004
                    $maskArray = $fileArray['m.'];
4005
                    // Must render mask images and include in hash-calculating
4006
                    // - otherwise we cannot be sure the filename is unique for the setup!
4007
                    if (is_array($maskArray)) {
4008
                        $mask = $this->getImgResource($maskArray['mask'], $maskArray['mask.']);
4009
                        $bgImg = $this->getImgResource($maskArray['bgImg'], $maskArray['bgImg.']);
4010
                        $bottomImg = $this->getImgResource($maskArray['bottomImg'], $maskArray['bottomImg.']);
4011
                        $bottomImg_mask = $this->getImgResource($maskArray['bottomImg_mask'], $maskArray['bottomImg_mask.']);
4012
4013
                        $processingConfiguration['maskImages']['maskImage'] = $mask['processedFile'];
4014
                        $processingConfiguration['maskImages']['backgroundImage'] = $bgImg['processedFile'];
4015
                        $processingConfiguration['maskImages']['maskBottomImage'] = $bottomImg['processedFile'];
4016
                        $processingConfiguration['maskImages']['maskBottomImageMask'] = $bottomImg_mask['processedFile'];
4017
                    }
4018
                    $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
4019
                    if ($processedFileObject->isProcessed()) {
4020
                        $imageResource = [
4021
                            0 => (int)$processedFileObject->getProperty('width'),
4022
                            1 => (int)$processedFileObject->getProperty('height'),
4023
                            2 => $processedFileObject->getExtension(),
4024
                            3 => $processedFileObject->getPublicUrl(),
4025
                            'origFile' => $fileObject->getPublicUrl(),
4026
                            'origFile_mtime' => $fileObject->getModificationTime(),
4027
                            // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
4028
                            // in order for the setup-array to create a unique filename hash.
4029
                            'originalFile' => $fileObject,
4030
                            'processedFile' => $processedFileObject
4031
                        ];
4032
                    }
4033
                }
4034
            }
4035
        }
4036
        // If image was processed by GIFBUILDER:
4037
        // ($imageResource indicates that it was processed the regular way)
4038
        if (!isset($imageResource)) {
4039
            try {
4040
                $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize((string)$file);
4041
                $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB');
4042
                $info['origFile'] = $theImage;
4043
                // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash.
4044
                $info['origFile_mtime'] = @filemtime($theImage);
4045
                $imageResource = $info;
4046
            } catch (Exception $e) {
4047
                // do nothing in case the file path is invalid
4048
            }
4049
        }
4050
        // Hook 'getImgResource': Post-processing of image resources
4051
        if (isset($imageResource)) {
4052
            /** @var ContentObjectGetImageResourceHookInterface $hookObject */
4053
            foreach ($this->getGetImgResourceHookObjects() as $hookObject) {
4054
                $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this);
4055
            }
4056
        }
4057
        return $imageResource;
4058
    }
4059
4060
    /**
4061
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4062
     * or null if the crop settings or crop area is empty.
4063
     *
4064
     * The cropArea from file reference is used, if not set in TypoScript.
4065
     *
4066
     * Example TypoScript settings:
4067
     * file.crop =
4068
     * OR
4069
     * file.crop = 50,50,100,100
4070
     * OR
4071
     * file.crop.data = file:current:crop
4072
     *
4073
     * @param FileReference $fileReference
4074
     * @param array $fileArray TypoScript properties for the imgResource type
4075
     * @return Area|null
4076
     */
4077
    protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray)
4078
    {
4079
        // Use cropping area from file reference if nothing is configured in TypoScript.
4080
        if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) {
4081
            // Set crop variant from TypoScript settings. If not set, use default.
4082
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4083
            $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant);
4084
            return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference);
4085
        }
4086
4087
        return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray);
4088
    }
4089
4090
    /**
4091
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4092
     * or null if the crop settings or crop area is empty.
4093
     *
4094
     * @param FileInterface $file
4095
     * @param array $fileArray
4096
     * @return Area|null
4097
     */
4098
    protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray)
4099
    {
4100
        /** @var Area $cropArea */
4101
        $cropArea = null;
4102
        // Resolve TypoScript configured cropping.
4103
        $cropSettings = isset($fileArray['crop.'])
4104
            ? $this->stdWrap($fileArray['crop'], $fileArray['crop.'])
4105
            : ($fileArray['crop'] ?? null);
4106
4107
        if (is_string($cropSettings)) {
4108
            // Set crop variant from TypoScript settings. If not set, use default.
4109
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4110
            // Get cropArea from CropVariantCollection, if cropSettings is a valid json.
4111
            // CropVariantCollection::create does json_decode.
4112
            $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant);
4113
            $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file);
4114
4115
            // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100
4116
            if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) {
4117
                $cropSettings = explode(',', $cropSettings);
4118
                if (count($cropSettings) === 4) {
4119
                    $stringCropArea = GeneralUtility::makeInstance(
4120
                        Area::class,
4121
                        ...$cropSettings
4122
                    );
4123
                    $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea;
4124
                }
4125
            }
4126
        }
4127
4128
        return $cropArea;
4129
    }
4130
4131
    /**
4132
     * Takes a JSON string and creates CropVariantCollection and fetches the corresponding
4133
     * CropArea for that.
4134
     *
4135
     * @param string $cropSettings
4136
     * @param string $cropVariant
4137
     * @return Area
4138
     */
4139
    protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area
4140
    {
4141
        return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant);
4142
    }
4143
4144
    /***********************************************
4145
     *
4146
     * Data retrieval etc.
4147
     *
4148
     ***********************************************/
4149
    /**
4150
     * 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.
4151
     *
4152
     * @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)
4153
     * @return string|null
4154
     */
4155
    public function getFieldVal($field)
4156
    {
4157
        if (strpos($field, '//') === false) {
4158
            return $this->data[trim($field)] ?? null;
4159
        }
4160
        $sections = GeneralUtility::trimExplode('//', $field, true);
4161
        foreach ($sections as $k) {
4162
            if ((string)($this->data[$k] ?? '') !== '') {
4163
                return $this->data[$k];
4164
            }
4165
        }
4166
4167
        return '';
4168
    }
4169
4170
    /**
4171
     * 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.
4172
     *
4173
     * @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)
4174
     * @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.
4175
     * @return string The value fetched
4176
     * @see getFieldVal()
4177
     */
4178
    public function getData($string, $fieldArray = null)
4179
    {
4180
        $tsfe = $this->getTypoScriptFrontendController();
4181
        if (!is_array($fieldArray)) {
4182
            $fieldArray = $tsfe->page;
4183
        }
4184
        $retVal = '';
4185
        $sections = explode('//', $string);
4186
        foreach ($sections as $secKey => $secVal) {
4187
            if ($retVal) {
4188
                break;
4189
            }
4190
            $parts = explode(':', $secVal, 2);
4191
            $type = strtolower(trim($parts[0]));
4192
            $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout'];
4193
            $key = trim($parts[1] ?? '');
4194
            if (($key != '') || in_array($type, $typesWithOutParameters)) {
4195
                switch ($type) {
4196
                    case 'gp':
4197
                        // Merge GET and POST and get $key out of the merged array
4198
                        $getPostArray = GeneralUtility::_GET();
4199
                        ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST());
4200
                        $retVal = $this->getGlobal($key, $getPostArray);
4201
                        break;
4202
                    case 'tsfe':
4203
                        $retVal = $this->getGlobal('TSFE|' . $key);
4204
                        break;
4205
                    case 'getenv':
4206
                        $retVal = getenv($key);
4207
                        break;
4208
                    case 'getindpenv':
4209
                        $retVal = $this->getEnvironmentVariable($key);
4210
                        break;
4211
                    case 'field':
4212
                        $retVal = $this->getGlobal($key, $fieldArray);
4213
                        break;
4214
                    case 'file':
4215
                        $retVal = $this->getFileDataKey($key);
4216
                        break;
4217
                    case 'parameters':
4218
                        $retVal = $this->parameters[$key] ?? null;
4219
                        break;
4220
                    case 'register':
4221
                        $retVal = $tsfe->register[$key] ?? null;
4222
                        break;
4223
                    case 'global':
4224
                        $retVal = $this->getGlobal($key);
4225
                        break;
4226
                    case 'level':
4227
                        $retVal = count($tsfe->tmpl->rootLine) - 1;
4228
                        break;
4229
                    case 'leveltitle':
4230
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4231
                        $pointer = (int)($keyParts[0] ?? 0);
4232
                        $slide = (string)($keyParts[1] ?? '');
4233
4234
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4235
                        $retVal = $this->rootLineValue($numericKey, 'title', strtolower($slide) === 'slide');
4236
                        break;
4237
                    case 'levelmedia':
4238
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4239
                        $pointer = (int)($keyParts[0] ?? 0);
4240
                        $slide = (string)($keyParts[1] ?? '');
4241
4242
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4243
                        $retVal = $this->rootLineValue($numericKey, 'media', strtolower($slide) === 'slide');
4244
                        break;
4245
                    case 'leveluid':
4246
                        $numericKey = $this->getKey((int)$key, $tsfe->tmpl->rootLine);
4247
                        $retVal = $this->rootLineValue($numericKey, 'uid');
4248
                        break;
4249
                    case 'levelfield':
4250
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4251
                        $pointer = (int)($keyParts[0] ?? 0);
4252
                        $field = (string)($keyParts[1] ?? '');
4253
                        $slide = (string)($keyParts[2] ?? '');
4254
4255
                        $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4256
                        $retVal = $this->rootLineValue($numericKey, $field, strtolower($slide) === 'slide');
4257
                        break;
4258
                    case 'fullrootline':
4259
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4260
                        $pointer = (int)($keyParts[0] ?? 0);
4261
                        $field = (string)($keyParts[1] ?? '');
4262
                        $slide = (string)($keyParts[2] ?? '');
4263
4264
                        $fullKey = (int)($pointer - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine));
4265
                        if ($fullKey >= 0) {
4266
                            $retVal = $this->rootLineValue($fullKey, $field, stristr($slide, 'slide') !== false, $tsfe->rootLine);
4267
                        }
4268
                        break;
4269
                    case 'date':
4270
                        if (!$key) {
4271
                            $key = 'd/m Y';
4272
                        }
4273
                        $retVal = date($key, $GLOBALS['EXEC_TIME']);
4274
                        break;
4275
                    case 'page':
4276
                        $retVal = $tsfe->page[$key];
4277
                        break;
4278
                    case 'pagelayout':
4279
                        $retVal = GeneralUtility::makeInstance(PageLayoutResolver::class)
4280
                            ->getLayoutForPage($tsfe->page, $tsfe->rootLine);
4281
                        break;
4282
                    case 'current':
4283
                        $retVal = $this->data[$this->currentValKey] ?? null;
4284
                        break;
4285
                    case 'db':
4286
                        $selectParts = GeneralUtility::trimExplode(':', $key);
4287
                        $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]);
4288
                        if (is_array($db_rec) && $selectParts[2]) {
4289
                            $retVal = $db_rec[$selectParts[2]];
4290
                        }
4291
                        break;
4292
                    case 'lll':
4293
                        $retVal = $tsfe->sL('LLL:' . $key);
4294
                        break;
4295
                    case 'path':
4296
                        try {
4297
                            $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key);
4298
                        } catch (Exception $e) {
4299
                            // do nothing in case the file path is invalid
4300
                            $retVal = null;
4301
                        }
4302
                        break;
4303
                    case 'cobj':
4304
                        switch ($key) {
4305
                            case 'parentRecordNumber':
4306
                                $retVal = $this->parentRecordNumber;
4307
                                break;
4308
                        }
4309
                        break;
4310
                    case 'debug':
4311
                        switch ($key) {
4312
                            case 'rootLine':
4313
                                $retVal = DebugUtility::viewArray($tsfe->tmpl->rootLine);
4314
                                break;
4315
                            case 'fullRootLine':
4316
                                $retVal = DebugUtility::viewArray($tsfe->rootLine);
4317
                                break;
4318
                            case 'data':
4319
                                $retVal = DebugUtility::viewArray($this->data);
4320
                                break;
4321
                            case 'register':
4322
                                $retVal = DebugUtility::viewArray($tsfe->register);
4323
                                break;
4324
                            case 'page':
4325
                                $retVal = DebugUtility::viewArray($tsfe->page);
4326
                                break;
4327
                        }
4328
                        break;
4329
                    case 'flexform':
4330
                        $keyParts = GeneralUtility::trimExplode(':', $key, true);
4331
                        if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) {
4332
                            $flexFormContent = $this->data[$keyParts[0]];
4333
                            if (!empty($flexFormContent)) {
4334
                                $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
4335
                                $flexFormKey = str_replace('.', '|', $keyParts[1]);
4336
                                $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent);
4337
                                $retVal = $this->getGlobal($flexFormKey, $settings);
4338
                            }
4339
                        }
4340
                        break;
4341
                    case 'session':
4342
                        $keyParts = GeneralUtility::trimExplode('|', $key, true);
4343
                        $sessionKey = array_shift($keyParts);
4344
                        $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey);
4345
                        foreach ($keyParts as $keyPart) {
4346
                            if (is_object($retVal)) {
4347
                                $retVal = $retVal->{$keyPart};
4348
                            } elseif (is_array($retVal)) {
4349
                                $retVal = $retVal[$keyPart];
4350
                            } else {
4351
                                $retVal = '';
4352
                                break;
4353
                            }
4354
                        }
4355
                        if (!is_scalar($retVal)) {
4356
                            $retVal = '';
4357
                        }
4358
                        break;
4359
                    case 'context':
4360
                        $context = GeneralUtility::makeInstance(Context::class);
4361
                        [$aspectName, $propertyName] = GeneralUtility::trimExplode(':', $key, true, 2);
4362
                        $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, '');
4363
                        if (is_array($retVal)) {
4364
                            $retVal = implode(',', $retVal);
4365
                        }
4366
                        if (!is_scalar($retVal)) {
4367
                            $retVal = '';
4368
                        }
4369
                        break;
4370
                    case 'site':
4371
                        $site = $this->getTypoScriptFrontendController()->getSite();
4372
                        if ($key === 'identifier') {
4373
                            $retVal = $site->getIdentifier();
4374
                        } elseif ($key === 'base') {
4375
                            $retVal = $site->getBase();
4376
                        } else {
4377
                            try {
4378
                                $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.');
0 ignored issues
show
Bug introduced by
The method getConfiguration() does not exist on TYPO3\CMS\Core\Site\Entity\SiteInterface. It seems like you code against a sub-type of TYPO3\CMS\Core\Site\Entity\SiteInterface such as TYPO3\CMS\Core\Site\Entity\Site. ( Ignorable by Annotation )

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

4378
                                $retVal = ArrayUtility::getValueByPath($site->/** @scrutinizer ignore-call */ getConfiguration(), $key, '.');
Loading history...
4379
                            } catch (MissingArrayPathException $exception) {
4380
                                $this->logger->warning('getData() with "{key}" failed', ['key' => $key, 'exception' => $exception]);
4381
                            }
4382
                        }
4383
                        break;
4384
                    case 'sitelanguage':
4385
                        $siteLanguage = $this->getTypoScriptFrontendController()->getLanguage();
4386
                        $config = $siteLanguage->toArray();
4387
                        if (isset($config[$key])) {
4388
                            $retVal = $config[$key];
4389
                        }
4390
                        break;
4391
                }
4392
            }
4393
4394
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) {
4395
                $hookObject = GeneralUtility::makeInstance($className);
4396
                if (!$hookObject instanceof ContentObjectGetDataHookInterface) {
4397
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480);
4398
                }
4399
                $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4400
                $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $ref);
4401
            }
4402
        }
4403
        return $retVal;
4404
    }
4405
4406
    /**
4407
     * Gets file information. This is a helper function for the getData() method above, which resolves e.g.
4408
     * page.10.data = file:current:title
4409
     * or
4410
     * page.10.data = file:17:title
4411
     *
4412
     * @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.)
4413
     * @return string|int The value as retrieved from the file object.
4414
     */
4415
    protected function getFileDataKey($key)
4416
    {
4417
        [$fileUidOrCurrentKeyword, $requestedFileInformationKey] = GeneralUtility::trimExplode(':', $key, false, 3);
4418
        try {
4419
            if ($fileUidOrCurrentKeyword === 'current') {
4420
                $fileObject = $this->getCurrentFile();
4421
            } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) {
4422
                /** @var ResourceFactory $fileFactory */
4423
                $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
4424
                $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword);
4425
            } else {
4426
                $fileObject = null;
4427
            }
4428
        } catch (Exception $exception) {
4429
            $this->logger->warning('The file "{uid}" could not be found and won\'t be included in frontend output', ['uid' => $fileUidOrCurrentKeyword, 'exception' => $exception]);
4430
            $fileObject = null;
4431
        }
4432
4433
        if ($fileObject instanceof FileInterface) {
4434
            // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here:
4435
            switch ($requestedFileInformationKey) {
4436
                case 'name':
4437
                    return $fileObject->getName();
4438
                case 'uid':
4439
                    if (method_exists($fileObject, 'getUid')) {
4440
                        return $fileObject->getUid();
4441
                    }
4442
                    return 0;
4443
                case 'originalUid':
4444
                    if ($fileObject instanceof FileReference) {
0 ignored issues
show
introduced by
$fileObject is never a sub-type of TYPO3\CMS\Core\Resource\FileReference.
Loading history...
4445
                        return $fileObject->getOriginalFile()->getUid();
4446
                    }
4447
                    return null;
4448
                case 'size':
4449
                    return $fileObject->getSize();
4450
                case 'sha1':
4451
                    return $fileObject->getSha1();
4452
                case 'extension':
4453
                    return $fileObject->getExtension();
4454
                case 'mimetype':
4455
                    return $fileObject->getMimeType();
4456
                case 'contents':
4457
                    return $fileObject->getContents();
4458
                case 'publicUrl':
4459
                    return $fileObject->getPublicUrl();
4460
                default:
4461
                    // Generic alternative here
4462
                    return $fileObject->getProperty($requestedFileInformationKey);
4463
            }
4464
        } else {
4465
            // @todo fail silently as is common in tslib_content
4466
            return 'Error: no file object';
4467
        }
4468
    }
4469
4470
    /**
4471
     * Returns a value from the current rootline (site) from $GLOBALS['TSFE']->tmpl->rootLine;
4472
     *
4473
     * @param int $key Which level in the root line
4474
     * @param string $field The field in the rootline record to return (a field from the pages table)
4475
     * @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
4476
     * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array
4477
     * @return string The value from the field of the rootline.
4478
     * @internal
4479
     * @see getData()
4480
     */
4481
    public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '')
4482
    {
4483
        $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine;
4484
        if (!$slideBack) {
4485
            return $rootLine[$key][$field];
4486
        }
4487
        for ($a = $key; $a >= 0; $a--) {
4488
            $val = $rootLine[$a][$field];
4489
            if ($val) {
4490
                return $val;
4491
            }
4492
        }
4493
4494
        return '';
4495
    }
4496
4497
    /**
4498
     * Return global variable where the input string $var defines array keys separated by "|"
4499
     * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
4500
     *
4501
     * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
4502
     * @param array $source Alternative array than $GLOBAL to get variables from.
4503
     * @return mixed Whatever value. If none, then blank string.
4504
     * @see getData()
4505
     */
4506
    public function getGlobal($keyString, $source = null)
4507
    {
4508
        $keys = explode('|', $keyString);
4509
        $numberOfLevels = count($keys);
4510
        $rootKey = trim($keys[0]);
4511
        $value = isset($source) ? ($source[$rootKey] ?? '') : ($GLOBALS[$rootKey] ?? '');
4512
        for ($i = 1; $i < $numberOfLevels && isset($value); $i++) {
4513
            $currentKey = trim($keys[$i]);
4514
            if (is_object($value)) {
4515
                $value = $value->{$currentKey};
4516
            } elseif (is_array($value)) {
4517
                $value = $value[$currentKey] ?? '';
4518
            } else {
4519
                $value = '';
4520
                break;
4521
            }
4522
        }
4523
        if (!is_scalar($value)) {
4524
            $value = '';
4525
        }
4526
        return $value;
4527
    }
4528
4529
    /**
4530
     * 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).
4531
     * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost...
4532
     *
4533
     * @param int $key The integer to transform
4534
     * @param array $arr array in which the key should be found.
4535
     * @return int The processed integer key value.
4536
     * @internal
4537
     * @see getData()
4538
     */
4539
    public function getKey($key, $arr)
4540
    {
4541
        $key = (int)$key;
4542
        if (is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
4543
            if ($key < 0) {
4544
                $key = count($arr) + $key;
4545
            }
4546
            if ($key < 0) {
4547
                $key = 0;
4548
            }
4549
        }
4550
        return $key;
4551
    }
4552
4553
    /***********************************************
4554
     *
4555
     * Link functions (typolink)
4556
     *
4557
     ***********************************************/
4558
    /**
4559
     * called from the typoLink() function
4560
     *
4561
     * does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1"
4562
     * into separate parts
4563
     *
4564
     * @param string $linkText The string (text) to link
4565
     * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
4566
     * @param array $configuration TypoScript configuration
4567
     * @return array|string
4568
     * @see typoLink()
4569
     *
4570
     * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links
4571
     */
4572
    protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
4573
    {
4574
        // Link parameter value = first part
4575
        $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
4576
4577
        // Check for link-handler keyword
4578
        $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2);
4579
        $linkHandlerKeyword = (string)($linkHandlerExploded[0] ?? '');
4580
4581
        if (in_array(strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
4582
            // Disallow insecure scheme's like javascript: or data:
4583
            return $linkText;
4584
        }
4585
4586
        // additional parameters that need to be set
4587
        if ($linkParameterParts['additionalParams'] !== '') {
4588
            $forceParams = $linkParameterParts['additionalParams'];
4589
            // params value
4590
            $configuration['additionalParams'] .= $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
4591
        }
4592
4593
        return [
4594
            'href'   => $linkParameterParts['url'],
4595
            'target' => $linkParameterParts['target'],
4596
            'class'  => $linkParameterParts['class'],
4597
            'title'  => $linkParameterParts['title']
4598
        ];
4599
    }
4600
4601
    /**
4602
     * Implements the "typolink" property of stdWrap (and others)
4603
     * 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.
4604
     * 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.
4605
     * 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.
4606
     * For many more details on the parameters and how they are interpreted, please see the link to TSref below.
4607
     *
4608
     * the FAL API is handled with the namespace/prefix "file:..."
4609
     *
4610
     * @param string $linkText The string (text) to link
4611
     * @param array $conf TypoScript configuration (see link below)
4612
     * @return string A link-wrapped string.
4613
     * @see stdWrap()
4614
     * @see \TYPO3\CMS\Frontend\Plugin\AbstractPlugin::pi_linkTP()
4615
     */
4616
    public function typoLink($linkText, $conf)
4617
    {
4618
        $linkText = (string)$linkText;
4619
        $tsfe = $this->getTypoScriptFrontendController();
4620
4621
        $linkParameter = trim((string)$this->stdWrapValue('parameter', $conf ?? []));
4622
        $this->lastTypoLinkUrl = '';
4623
        $this->lastTypoLinkTarget = '';
4624
4625
        $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf);
4626
        // check if the link handler hook has resolved the link completely already
4627
        if (!is_array($resolvedLinkParameters)) {
4628
            return $resolvedLinkParameters;
4629
        }
4630
        $linkParameter = $resolvedLinkParameters['href'];
4631
        $target = $resolvedLinkParameters['target'];
4632
        $title = $resolvedLinkParameters['title'];
4633
4634
        if (!$linkParameter) {
4635
            return $this->resolveAnchorLink($linkText, $conf ?? []);
4636
        }
4637
4638
        // Detecting kind of link and resolve all necessary parameters
4639
        $linkService = GeneralUtility::makeInstance(LinkService::class);
4640
        try {
4641
            $linkDetails = $linkService->resolve($linkParameter);
4642
        } catch (UnknownLinkHandlerException | InvalidPathException $exception) {
4643
            $this->logger->warning('The link could not be generated', ['exception' => $exception]);
4644
            return $linkText;
4645
        }
4646
4647
        $linkDetails['typoLinkParameter'] = $linkParameter;
4648
        if (isset($linkDetails['type']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
4649
            /** @var AbstractTypolinkBuilder $linkBuilder */
4650
            $linkBuilder = GeneralUtility::makeInstance(
4651
                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
4652
                $this,
4653
                $tsfe
4654
            );
4655
            try {
4656
                [$this->lastTypoLinkUrl, $linkText, $target] = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
4657
                $this->lastTypoLinkTarget = htmlspecialchars($target);
4658
                $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
4659
                $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
4660
            } catch (UnableToLinkException $e) {
4661
                $this->logger->debug('Unable to link "{text}"', [
4662
                    'text' => $e->getLinkText(),
4663
                    'exception' => $e,
4664
                ]);
4665
4666
                // Only return the link text directly
4667
                return $e->getLinkText();
4668
            }
4669
        } elseif (isset($linkDetails['url'])) {
4670
            $this->lastTypoLinkUrl = $linkDetails['url'];
4671
            $this->lastTypoLinkTarget = htmlspecialchars($target);
4672
            $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
4673
            $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
4674
        } else {
4675
            return $linkText;
4676
        }
4677
4678
        // We need to backup the URL because ATagParams might call typolink again and change the last URL.
4679
        $url = $this->lastTypoLinkUrl;
4680
        $finalTagParts = [
4681
            'aTagParams' => $this->getATagParams($conf),
4682
            'url'        => $url,
4683
            'TYPE'       => $linkDetails['type']
4684
        ];
4685
4686
        // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
4687
        if (!empty($finalTagParts['aTagParams'])) {
4688
            $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'], true);
4689
            if (isset($aTagParams['href'])) {
4690
                unset($aTagParams['href']);
4691
                $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams, true);
4692
            }
4693
        }
4694
4695
        // Building the final <a href=".."> tag
4696
        $tagAttributes = [];
4697
4698
        // Title attribute
4699
        if (empty($title)) {
4700
            $title = $conf['title'] ?? '';
4701
            if (isset($conf['title.']) && is_array($conf['title.'])) {
4702
                $title = $this->stdWrap($title, $conf['title.']);
4703
            }
4704
        }
4705
4706
        // Check, if the target is coded as a JS open window link:
4707
        $JSwindowParts = [];
4708
        $JSwindowParams = '';
4709
        if ($target && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $target, $JSwindowParts)) {
4710
            // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
4711
            $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower(($conf['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
4712
            $JSwindow_paramsArr = [];
4713
            $target = $conf['target'] ?? 'FEopenLink';
4714
            foreach ($JSwindow_tempParamsArr as $JSv) {
4715
                [$JSp, $JSv] = explode('=', $JSv, 2);
4716
                // If the target is set as JS param, this is extracted
4717
                if ($JSp === 'target') {
4718
                    $target = $JSv;
4719
                } else {
4720
                    $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
4721
                }
4722
            }
4723
            // Add width/height:
4724
            $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
4725
            $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
4726
            // Imploding into string:
4727
            $JSwindowParams = implode(',', $JSwindow_paramsArr);
4728
        }
4729
4730
        if (!$JSwindowParams && $linkDetails['type'] === LinkService::TYPE_EMAIL && $tsfe->spamProtectEmailAddresses === 'ascii') {
4731
            $tagAttributes['href'] = $finalTagParts['url'];
4732
        } else {
4733
            $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']);
4734
        }
4735
        if (!empty($title)) {
4736
            $tagAttributes['title'] = htmlspecialchars($title);
4737
        }
4738
4739
        // Target attribute
4740
        if (!empty($target)) {
4741
            $tagAttributes['target'] = htmlspecialchars($target);
4742
        }
4743
        if ($JSwindowParams && in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
4744
            // Create TARGET-attribute only if the right doctype is used
4745
            unset($tagAttributes['target']);
4746
        }
4747
4748
        if ($JSwindowParams) {
4749
            $onClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($finalTagParts['url']))
4750
                . ',' . GeneralUtility::quoteJSvalue($target) . ','
4751
                . GeneralUtility::quoteJSvalue($JSwindowParams)
4752
                . ');vHWin.focus();return false;';
4753
            $tagAttributes['onclick'] = htmlspecialchars($onClick);
4754
        }
4755
4756
        if (!empty($resolvedLinkParameters['class'])) {
4757
            $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']);
4758
        }
4759
4760
        // Prevent trouble with double and missing spaces between attributes and merge params before implode
4761
        // (skip decoding HTML entities, since `$tagAttributes` are expected to be encoded already)
4762
        $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
4763
        $finalTagAttributes = $this->addSecurityRelValues($finalTagAttributes, $target, $tagAttributes['href']);
4764
        $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
4765
4766
        // kept for backwards-compatibility in hooks
4767
        $finalTagParts['targetParams'] = !empty($tagAttributes['target']) ? ' target="' . $tagAttributes['target'] . '"' : '';
4768
        $this->lastTypoLinkTarget = $target;
4769
4770
        // Call user function:
4771
        if ($conf['userFunc'] ?? false) {
4772
            $finalTagParts['TAG'] = $finalAnchorTag;
4773
            $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $finalTagParts);
4774
        }
4775
4776
        // Hook: Call post processing function for link rendering:
4777
        $_params = [
4778
            'conf' => &$conf,
4779
            'linktxt' => &$linkText,
4780
            'finalTag' => &$finalAnchorTag,
4781
            'finalTagParts' => &$finalTagParts,
4782
            'linkDetails' => &$linkDetails,
4783
            'tagAttributes' => &$finalTagAttributes
4784
        ];
4785
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) {
4786
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4787
            GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
4788
        }
4789
4790
        // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made:
4791
        if ($conf['returnLast'] ?? false) {
4792
            switch ($conf['returnLast']) {
4793
                case 'url':
4794
                    return $this->lastTypoLinkUrl;
4795
                case 'target':
4796
                    return $this->lastTypoLinkTarget;
4797
            }
4798
        }
4799
4800
        $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
4801
4802
        if ($conf['ATagBeforeWrap'] ?? false) {
4803
            return $finalAnchorTag . $this->wrap($linkText, $wrap) . '</a>';
4804
        }
4805
        return $this->wrap($finalAnchorTag . $linkText . '</a>', $wrap);
4806
    }
4807
4808
    protected function addSecurityRelValues(array $tagAttributes, ?string $target, string $url): array
4809
    {
4810
        $relAttribute = 'noreferrer';
4811
        if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($url)) {
4812
            return $tagAttributes;
4813
        }
4814
4815
        if (!isset($tagAttributes['rel'])) {
4816
            $tagAttributes['rel'] = $relAttribute;
4817
            return $tagAttributes;
4818
        }
4819
4820
        $tagAttributes['rel'] = implode(' ', array_unique(array_merge(
4821
            GeneralUtility::trimExplode(' ', $relAttribute),
4822
            GeneralUtility::trimExplode(' ', $tagAttributes['rel'])
4823
        )));
4824
4825
        return $tagAttributes;
4826
    }
4827
4828
    /**
4829
     * Checks whether the given url is an internal url.
4830
     *
4831
     * It will check the host part only, against all configured sites
4832
     * whether the given host is any. If so, the url is considered internal
4833
     *
4834
     * @param string $url The url to check.
4835
     * @return bool
4836
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
4837
     */
4838
    protected function isInternalUrl(string $url): bool
4839
    {
4840
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
4841
        $parsedUrl = parse_url($url);
4842
        $foundDomains = 0;
4843
        if (!isset($parsedUrl['host'])) {
4844
            return true;
4845
        }
4846
4847
        $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
4848
4849
        if ($cache->has($cacheIdentifier) === false) {
4850
            foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
4851
                if ($site->getBase()->getHost() === $parsedUrl['host']) {
4852
                    ++$foundDomains;
4853
                    break;
4854
                }
4855
4856
                if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url)) {
4857
                    ++$foundDomains;
4858
                    break;
4859
                }
4860
            }
4861
4862
            $cache->set($cacheIdentifier, $foundDomains > 0);
4863
        }
4864
4865
        return (bool)$cache->get($cacheIdentifier);
4866
    }
4867
4868
    /**
4869
     * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
4870
     *
4871
     * @param array $conf TypoScript properties for "typolink
4872
     * @return string The URL of the link-tag that typolink() would by itself return
4873
     * @see typoLink()
4874
     */
4875
    public function typoLink_URL($conf)
4876
    {
4877
        $this->typoLink('|', $conf);
4878
        return $this->lastTypoLinkUrl;
4879
    }
4880
4881
    /**
4882
     * Returns a linked string made from typoLink parameters.
4883
     *
4884
     * 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.
4885
     * Optionally you can supply $urlParameters which is an array with key/value pairs that are rawurlencoded and appended to the resulting url.
4886
     *
4887
     * @param string $label Text string being wrapped by the link.
4888
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
4889
     * @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.
4890
     * @param string $target Specific target set, if any. (Default is using the current)
4891
     * @return string The wrapped $label-text string
4892
     * @see getTypoLink_URL()
4893
     */
4894
    public function getTypoLink($label, $params, $urlParameters = [], $target = '')
4895
    {
4896
        $conf = [];
4897
        $conf['parameter'] = $params;
4898
        if ($target) {
4899
            $conf['target'] = $target;
4900
            $conf['extTarget'] = $target;
4901
            $conf['fileTarget'] = $target;
4902
        }
4903
        if (is_array($urlParameters)) {
4904
            if (!empty($urlParameters)) {
4905
                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
4906
            }
4907
        } else {
4908
            $conf['additionalParams'] .= $urlParameters;
4909
        }
4910
        $out = $this->typoLink($label, $conf);
4911
        return $out;
4912
    }
4913
4914
    /**
4915
     * Returns the canonical URL to the current "location", which include the current page ID and type
4916
     * and optionally the query string
4917
     *
4918
     * @param bool $addQueryString Whether additional GET arguments in the query string should be included or not
4919
     * @return string
4920
     */
4921
    public function getUrlToCurrentLocation($addQueryString = true)
4922
    {
4923
        $conf = [];
4924
        $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type;
4925
        if ($addQueryString) {
4926
            $conf['addQueryString'] = '1';
4927
            $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars)));
4928
            $conf['addQueryString.'] = [
4929
                'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : '')
4930
            ];
4931
        }
4932
4933
        return $this->typoLink_URL($conf);
4934
    }
4935
4936
    /**
4937
     * Returns the URL of a "typolink" create from the input parameter string, url-parameters and target
4938
     *
4939
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
4940
     * @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.
4941
     * @param string $target Specific target set, if any. (Default is using the current)
4942
     * @return string The URL
4943
     * @see getTypoLink()
4944
     */
4945
    public function getTypoLink_URL($params, $urlParameters = [], $target = '')
4946
    {
4947
        $this->getTypoLink('', $params, $urlParameters, $target);
4948
        return $this->lastTypoLinkUrl;
4949
    }
4950
4951
    /**
4952
     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
4953
     *
4954
     * @param string $context The context in which the method is called (e.g. typoLink).
4955
     * @param string $url The URL that should be processed.
4956
     * @param array $typolinkConfiguration The current link configuration array.
4957
     * @return string|null Returns NULL if URL was not processed or the processed URL as a string.
4958
     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
4959
     */
4960
    protected function processUrl($context, $url, $typolinkConfiguration = [])
4961
    {
4962
        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
4963
        if (empty($urlProcessors)) {
4964
            return $url;
4965
        }
4966
4967
        foreach ($urlProcessors as $identifier => $configuration) {
4968
            if (empty($configuration) || !is_array($configuration)) {
4969
                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
4970
            }
4971
            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
4972
                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
4973
            }
4974
        }
4975
4976
        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
4977
        $keepProcessing = true;
4978
4979
        foreach ($orderedProcessors as $configuration) {
4980
            /** @var UrlProcessorInterface $urlProcessor */
4981
            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
4982
            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
4983
            if (!$keepProcessing) {
4984
                break;
4985
            }
4986
        }
4987
4988
        return $url;
4989
    }
4990
4991
    /**
4992
     * Creates a href attibute for given $mailAddress.
4993
     * The function uses spamProtectEmailAddresses for encoding the mailto statement.
4994
     * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:[email protected]".
4995
     *
4996
     * @param string $mailAddress Email address
4997
     * @param string $linktxt Link text, default will be the email address.
4998
     * @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.
4999
     */
5000
    public function getMailTo($mailAddress, $linktxt)
5001
    {
5002
        $mailAddress = (string)$mailAddress;
5003
        if ((string)$linktxt === '') {
5004
            $linktxt = htmlspecialchars($mailAddress);
5005
        }
5006
5007
        $originalMailToUrl = 'mailto:' . $mailAddress;
5008
        $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
5009
5010
        // no processing happened, therefore, the default processing kicks in
5011
        if ($mailToUrl === $originalMailToUrl) {
5012
            $tsfe = $this->getTypoScriptFrontendController();
5013
            if ($tsfe->spamProtectEmailAddresses) {
5014
                $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
5015
                if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
5016
                    $encodedForJsAndHref = rawurlencode(GeneralUtility::quoteJSvalue($mailToUrl));
5017
                    $mailToUrl = 'javascript:linkTo_UnCryptMailto(' . $encodedForJsAndHref . ');';
5018
                }
5019
                $atLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst']) ?: '(at)';
5020
                $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
5021
                if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']) {
5022
                    $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
5023
                    $lastDotLabel = $lastDotLabel ?: '(dot)';
5024
                    $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
5025
                    if ($spamProtectedMailAddress === null) {
5026
                        $this->logger->debug('Error replacing the last dot in email address "{email}"', ['email' => $spamProtectedMailAddress]);
5027
                        $spamProtectedMailAddress = '';
5028
                    }
5029
                }
5030
                $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
5031
            }
5032
        }
5033
5034
        return [$mailToUrl, $linktxt];
5035
    }
5036
5037
    /**
5038
     * Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
5039
     *
5040
     * @param string $string Input string to en/decode: "mailto:[email protected]
5041
     * @param mixed  $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses
5042
     * @return string encoded version of $string
5043
     */
5044
    protected function encryptEmail(string $string, $type): string
5045
    {
5046
        $out = '';
5047
        // obfuscates using the decimal HTML entity references for each character
5048
        if ($type === 'ascii') {
5049
            foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5050
                $out .= '&#' . mb_ord($char) . ';';
5051
            }
5052
        } else {
5053
            // like str_rot13() but with a variable offset and a wider character range
5054
            $len = strlen($string);
5055
            $offset = (int)$type;
5056
            for ($i = 0; $i < $len; $i++) {
5057
                $charValue = ord($string[$i]);
5058
                // 0-9 . , - + / :
5059
                if ($charValue >= 43 && $charValue <= 58) {
5060
                    $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5061
                } elseif ($charValue >= 64 && $charValue <= 90) {
5062
                    // A-Z @
5063
                    $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5064
                } elseif ($charValue >= 97 && $charValue <= 122) {
5065
                    // a-z
5066
                    $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5067
                } else {
5068
                    $out .= $string[$i];
5069
                }
5070
            }
5071
        }
5072
        return $out;
5073
    }
5074
5075
    /**
5076
     * Encryption (or decryption) of a single character.
5077
     * Within the given range the character is shifted with the supplied offset.
5078
     *
5079
     * @param int $n Ordinal of input character
5080
     * @param int $start Start of range
5081
     * @param int $end End of range
5082
     * @param int $offset Offset
5083
     * @return string encoded/decoded version of character
5084
     */
5085
    protected function encryptCharcode($n, $start, $end, $offset)
5086
    {
5087
        $n = $n + $offset;
5088
        if ($offset > 0 && $n > $end) {
5089
            $n = $start + ($n - $end - 1);
5090
        } elseif ($offset < 0 && $n < $start) {
5091
            $n = $end - ($start - $n - 1);
5092
        }
5093
        return chr($n);
5094
    }
5095
5096
    /**
5097
     * Gets the query arguments and assembles them for URLs.
5098
     * Arguments may be removed or set, depending on configuration.
5099
     *
5100
     * @param array $conf Configuration
5101
     * @return string The URL query part (starting with a &)
5102
     */
5103
    public function getQueryArguments($conf)
5104
    {
5105
        $currentQueryArray = GeneralUtility::_GET();
5106
        if ($conf['exclude'] ?? false) {
5107
            $excludeString = str_replace(',', '&', $conf['exclude']);
5108
            $excludedQueryParts = [];
5109
            parse_str($excludeString, $excludedQueryParts);
5110
            $newQueryArray = ArrayUtility::arrayDiffKeyRecursive($currentQueryArray, $excludedQueryParts);
5111
        } else {
5112
            $newQueryArray = $currentQueryArray;
5113
        }
5114
        return HttpUtility::buildQueryString($newQueryArray, '&');
5115
    }
5116
5117
    /***********************************************
5118
     *
5119
     * Miscellaneous functions, stand alone
5120
     *
5121
     ***********************************************/
5122
    /**
5123
     * Wrapping a string.
5124
     * Implements the TypoScript "wrap" property.
5125
     * Example: $content = "HELLO WORLD" and $wrap = "<strong> | </strong>", result: "<strong>HELLO WORLD</strong>"
5126
     *
5127
     * @param string $content The content to wrap
5128
     * @param string $wrap The wrap value, eg. "<strong> | </strong>
5129
     * @param string $char The char used to split the wrapping value, default is "|
5130
     * @return string Wrapped input string
5131
     * @see noTrimWrap()
5132
     */
5133
    public function wrap($content, $wrap, $char = '|')
5134
    {
5135
        if ($wrap) {
5136
            $wrapArr = explode($char, $wrap);
5137
            $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
5138
        }
5139
        return $content;
5140
    }
5141
5142
    /**
5143
     * Wrapping a string, preserving whitespace in wrap value.
5144
     * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does)
5145
     *
5146
     * @param string $content The content to wrap, eg. "HELLO WORLD
5147
     * @param string $wrap The wrap value, eg. " | <strong> | </strong>
5148
     * @param string $char The char used to split the wrapping value, default is "|"
5149
     * @return string Wrapped input string, eg. " <strong> HELLO WORD </strong>
5150
     * @see wrap()
5151
     */
5152
    public function noTrimWrap($content, $wrap, $char = '|')
5153
    {
5154
        if ($wrap) {
5155
            // expects to be wrapped with (at least) 3 characters (before, middle, after)
5156
            // anything else is not taken into account
5157
            $wrapArr = explode($char, $wrap, 4);
5158
            $content = $wrapArr[1] . $content . $wrapArr[2];
5159
        }
5160
        return $content;
5161
    }
5162
5163
    /**
5164
     * Calling a user function/class-method
5165
     * Notice: For classes the instantiated object will have the internal variable, $cObj, set to be a *reference* to $this (the parent/calling object).
5166
     *
5167
     * @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.
5168
     * @param array $conf The TypoScript configuration to pass the function
5169
     * @param mixed $content The content payload to pass the function
5170
     * @return mixed The return content from the function call. Should probably be a string.
5171
     * @see stdWrap()
5172
     * @see typoLink()
5173
     * @see _parseFunc()
5174
     */
5175
    public function callUserFunction($funcName, $conf, $content)
5176
    {
5177
        // Split parts
5178
        $parts = explode('->', $funcName);
5179
        if (count($parts) === 2) {
5180
            // Check whether PHP class is available
5181
            if (class_exists($parts[0])) {
5182
                if ($this->container && $this->container->has($parts[0])) {
5183
                    $classObj = $this->container->get($parts[0]);
5184
                } else {
5185
                    $classObj = GeneralUtility::makeInstance($parts[0]);
5186
                }
5187
                $methodName = (string)$parts[1];
5188
                $callable = [$classObj, $methodName];
5189
                if (is_object($classObj) && method_exists($classObj, $parts[1]) && is_callable($callable)) {
5190
                    $classObj->cObj = $this;
5191
                    $content = $callable($content, $conf, $this->getRequest());
5192
                } else {
5193
                    $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', 3);
5194
                }
5195
            } else {
5196
                $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', 3);
5197
            }
5198
        } elseif (function_exists($funcName)) {
5199
            $content = $funcName($content, $conf);
5200
        } else {
5201
            $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', 3);
5202
        }
5203
        return $content;
5204
    }
5205
5206
    /**
5207
     * Cleans up a string of keywords. Keywords at splitted by "," (comma)  ";" (semi colon) and linebreak
5208
     *
5209
     * @param string $content String of keywords
5210
     * @return string Cleaned up string, keywords will be separated by a comma only.
5211
     */
5212
    public function keywords($content)
5213
    {
5214
        $listArr = preg_split('/[,;' . LF . ']/', $content);
5215
        if ($listArr === false) {
5216
            return '';
5217
        }
5218
        foreach ($listArr as $k => $v) {
5219
            $listArr[$k] = trim($v);
5220
        }
5221
        return implode(',', $listArr);
5222
    }
5223
5224
    /**
5225
     * Changing character case of a string, converting typically used western charset characters as well.
5226
     *
5227
     * @param string $theValue The string to change case for.
5228
     * @param string $case The direction; either "upper" or "lower
5229
     * @return string
5230
     * @see HTMLcaseshift()
5231
     */
5232
    public function caseshift($theValue, $case)
5233
    {
5234
        switch (strtolower($case)) {
5235
            case 'upper':
5236
                $theValue = mb_strtoupper($theValue, 'utf-8');
5237
                break;
5238
            case 'lower':
5239
                $theValue = mb_strtolower($theValue, 'utf-8');
5240
                break;
5241
            case 'capitalize':
5242
                $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8');
5243
                break;
5244
            case 'ucfirst':
5245
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5246
                $firstChar = mb_strtoupper($firstChar, 'utf-8');
5247
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5248
                $theValue = $firstChar . $remainder;
5249
                break;
5250
            case 'lcfirst':
5251
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5252
                $firstChar = mb_strtolower($firstChar, 'utf-8');
5253
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5254
                $theValue = $firstChar . $remainder;
5255
                break;
5256
            case 'uppercamelcase':
5257
                $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue);
5258
                break;
5259
            case 'lowercamelcase':
5260
                $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue);
5261
                break;
5262
        }
5263
        return $theValue;
5264
    }
5265
5266
    /**
5267
     * Shifts the case of characters outside of HTML tags in the input string
5268
     *
5269
     * @param string $theValue The string to change case for.
5270
     * @param string $case The direction; either "upper" or "lower
5271
     * @return string
5272
     * @see caseshift()
5273
     */
5274
    public function HTMLcaseshift($theValue, $case)
5275
    {
5276
        $inside = 0;
5277
        $newVal = '';
5278
        $pointer = 0;
5279
        $totalLen = strlen($theValue);
5280
        do {
5281
            if (!$inside) {
5282
                $len = strcspn(substr($theValue, $pointer), '<');
5283
                $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case);
5284
                $inside = 1;
5285
            } else {
5286
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
5287
                $newVal .= substr($theValue, $pointer, $len);
5288
                $inside = 0;
5289
            }
5290
            $pointer += $len;
5291
        } while ($pointer < $totalLen);
5292
        return $newVal;
5293
    }
5294
5295
    /**
5296
     * Returns the 'age' of the tstamp $seconds
5297
     *
5298
     * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs
5299
     * @param string $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs'
5300
     * @return string The formatted string
5301
     */
5302
    public function calcAge($seconds, $labels)
5303
    {
5304
        if (MathUtility::canBeInterpretedAsInteger($labels)) {
5305
            $labels = ' min| hrs| days| yrs| min| hour| day| year';
5306
        } else {
5307
            $labels = str_replace('"', '', $labels);
5308
        }
5309
        $labelArr = explode('|', $labels);
5310
        if (count($labelArr) === 4) {
5311
            $labelArr = array_merge($labelArr, $labelArr);
5312
        }
5313
        $absSeconds = abs($seconds);
5314
        $sign = $seconds > 0 ? 1 : -1;
5315
        if ($absSeconds < 3600) {
5316
            $val = round($absSeconds / 60);
5317
            $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]);
5318
        } elseif ($absSeconds < 24 * 3600) {
5319
            $val = round($absSeconds / 3600);
5320
            $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]);
5321
        } elseif ($absSeconds < 365 * 24 * 3600) {
5322
            $val = round($absSeconds / (24 * 3600));
5323
            $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]);
5324
        } else {
5325
            $val = round($absSeconds / (365 * 24 * 3600));
5326
            $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null));
5327
        }
5328
        return $seconds;
5329
    }
5330
5331
    /**
5332
     * Resolves a TypoScript reference value to the full set of properties BUT overridden with any local properties set.
5333
     * So the reference is resolved but overlaid with local TypoScript properties of the reference value.
5334
     *
5335
     * @param array $confArr The TypoScript array
5336
     * @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.
5337
     * @return array The modified TypoScript array
5338
     */
5339
    public function mergeTSRef($confArr, $prop)
5340
    {
5341
        if ($confArr[$prop][0] === '<') {
5342
            $key = trim(substr($confArr[$prop], 1));
5343
            $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
5344
            // $name and $conf is loaded with the referenced values.
5345
            $old_conf = $confArr[$prop . '.'];
5346
            $conf = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup)[1] ?? [];
5347
            if (is_array($old_conf) && !empty($old_conf)) {
5348
                $conf = is_array($conf) ? array_replace_recursive($conf, $old_conf) : $old_conf;
5349
            }
5350
            $confArr[$prop . '.'] = $conf;
5351
        }
5352
        return $confArr;
5353
    }
5354
5355
    /***********************************************
5356
     *
5357
     * Database functions, making of queries
5358
     *
5359
     ***********************************************/
5360
    /**
5361
     * Generates a list of Page-uid's from $id. List does not include $id itself
5362
     * (unless the id specified is negative in which case it does!)
5363
     * The only pages WHICH PREVENTS DECENDING in a branch are
5364
     * - deleted pages,
5365
     * - pages in a recycler (doktype = 255) or of the Backend User Section (doktpe = 6) type
5366
     * - pages that has the extendToSubpages set, WHERE start/endtime, hidden
5367
     * and fe_users would hide the records.
5368
     * Apart from that, pages with enable-fields excluding them, will also be
5369
     * removed. HOWEVER $dontCheckEnableFields set will allow
5370
     * enableFields-excluded pages to be included anyway - including
5371
     * extendToSubpages sections!
5372
     * Mount Pages are also descended but notice that these ID numbers are not
5373
     * useful for links unless the correct MPvar is set.
5374
     *
5375
     * @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!
5376
     * @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...)
5377
     * @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'
5378
     * @param bool $dontCheckEnableFields See function description
5379
     * @param string $addSelectFields Additional fields to select. Syntax: ",[fieldname],[fieldname],...
5380
     * @param string $moreWhereClauses Additional where clauses. Syntax: " AND [fieldname]=[value] AND ...
5381
     * @param array $prevId_array array of IDs from previous recursions. In order to prevent infinite loops with mount pages.
5382
     * @param int $recursionLevel Internal: Zero for the first recursion, incremented for each recursive call.
5383
     * @return string Returns the list of ids as a comma separated string
5384
     * @see TypoScriptFrontendController::checkEnableFields()
5385
     * @see TypoScriptFrontendController::checkPagerecordForIncludeSection()
5386
     */
5387
    public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0)
5388
    {
5389
        $id = (int)$id;
5390
        if (!$id) {
5391
            return '';
5392
        }
5393
5394
        // Init vars:
5395
        $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state,l10n_parent' . $addSelectFields;
5396
        $depth = (int)$depth;
5397
        $begin = (int)$begin;
5398
        $theList = [];
5399
        $addId = 0;
5400
        $requestHash = '';
5401
5402
        // First level, check id (second level, this is done BEFORE the recursive call)
5403
        $tsfe = $this->getTypoScriptFrontendController();
5404
        if (!$recursionLevel) {
5405
            // Check tree list cache
5406
            // First, create the hash for this request - not sure yet whether we need all these parameters though
5407
            $parameters = [
5408
                $id,
5409
                $depth,
5410
                $begin,
5411
                $dontCheckEnableFields,
5412
                $addSelectFields,
5413
                $moreWhereClauses,
5414
                $prevId_array,
5415
                GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1])
5416
            ];
5417
            $requestHash = md5(serialize($parameters));
5418
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5419
                ->getQueryBuilderForTable('cache_treelist');
5420
            $cacheEntry = $queryBuilder->select('treelist')
5421
                ->from('cache_treelist')
5422
                ->where(
5423
                    $queryBuilder->expr()->eq(
5424
                        'md5hash',
5425
                        $queryBuilder->createNamedParameter($requestHash, \PDO::PARAM_STR)
5426
                    ),
5427
                    $queryBuilder->expr()->orX(
5428
                        $queryBuilder->expr()->gt(
5429
                            'expires',
5430
                            $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
5431
                        ),
5432
                        $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5433
                    )
5434
                )
5435
                ->setMaxResults(1)
5436
                ->execute()
5437
                ->fetch();
5438
5439
            if (is_array($cacheEntry)) {
5440
                // Cache hit
5441
                return $cacheEntry['treelist'];
5442
            }
5443
            // If Id less than zero it means we should add the real id to list:
5444
            if ($id < 0) {
5445
                $addId = $id = abs($id);
5446
            }
5447
            // Check start page:
5448
            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

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

5450
                $mount_info = $tsfe->sys_page->getMountPointInfo(/** @scrutinizer ignore-type */ $id);
Loading history...
5451
                if (is_array($mount_info)) {
5452
                    $id = $mount_info['mount_pid'];
5453
                    // In Overlay mode, use the mounted page uid as added ID!:
5454
                    if ($addId && $mount_info['overlay']) {
5455
                        $addId = $id;
5456
                    }
5457
                }
5458
            } else {
5459
                // Return blank if the start page was NOT found at all!
5460
                return '';
5461
            }
5462
        }
5463
        // Add this ID to the array of IDs
5464
        if ($begin <= 0) {
5465
            $prevId_array[] = $id;
5466
        }
5467
        // Select sublevel:
5468
        if ($depth > 0) {
5469
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
5470
            $queryBuilder->getRestrictions()
5471
                ->removeAll()
5472
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5473
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5474
                ->from('pages')
5475
                ->where(
5476
                    $queryBuilder->expr()->eq(
5477
                        'pid',
5478
                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
5479
                    ),
5480
                    // tree is only built by language=0 pages
5481
                    $queryBuilder->expr()->eq('sys_language_uid', 0)
5482
                )
5483
                ->orderBy('sorting');
5484
5485
            if (!empty($moreWhereClauses)) {
5486
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5487
            }
5488
5489
            $result = $queryBuilder->execute();
5490
            while ($row = $result->fetch()) {
5491
                /** @var VersionState $versionState */
5492
                $versionState = VersionState::cast($row['t3ver_state']);
5493
                $tsfe->sys_page->versionOL('pages', $row);
5494
                if ($row === false
5495
                    || (int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5496
                    || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5497
                    || $versionState->indicatesPlaceholder()
5498
                ) {
5499
                    // falsy row means Overlay prevents access to this page.
5500
                    // Doing this after the overlay to make sure changes
5501
                    // in the overlay are respected.
5502
                    // However, we do not process pages below of and
5503
                    // including of type recycler and BE user section
5504
                    continue;
5505
                }
5506
                // Find mount point if any:
5507
                $next_id = $row['uid'];
5508
                $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row);
5509
                // Overlay mode:
5510
                if (is_array($mount_info) && $mount_info['overlay']) {
5511
                    $next_id = $mount_info['mount_pid'];
5512
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5513
                        ->getQueryBuilderForTable('pages');
5514
                    $queryBuilder->getRestrictions()
5515
                        ->removeAll()
5516
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5517
                    $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5518
                        ->from('pages')
5519
                        ->where(
5520
                            $queryBuilder->expr()->eq(
5521
                                'uid',
5522
                                $queryBuilder->createNamedParameter($next_id, \PDO::PARAM_INT)
5523
                            )
5524
                        )
5525
                        ->orderBy('sorting')
5526
                        ->setMaxResults(1);
5527
5528
                    if (!empty($moreWhereClauses)) {
5529
                        $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5530
                    }
5531
5532
                    $row = $queryBuilder->execute()->fetch();
5533
                    $tsfe->sys_page->versionOL('pages', $row);
5534
                    if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5535
                        || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5536
                        || $versionState->indicatesPlaceholder()
5537
                    ) {
5538
                        // Doing this after the overlay to make sure
5539
                        // changes in the overlay are respected.
5540
                        // see above
5541
                        continue;
5542
                    }
5543
                }
5544
                // Add record:
5545
                if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) {
5546
                    // Add ID to list:
5547
                    if ($begin <= 0) {
5548
                        if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) {
5549
                            $theList[] = $next_id;
5550
                        }
5551
                    }
5552
                    // Next level:
5553
                    if ($depth > 1 && !$row['php_tree_stop']) {
5554
                        // Normal mode:
5555
                        if (is_array($mount_info) && !$mount_info['overlay']) {
5556
                            $next_id = $mount_info['mount_pid'];
5557
                        }
5558
                        // Call recursively, if the id is not in prevID_array:
5559
                        if (!in_array($next_id, $prevId_array)) {
5560
                            $theList = array_merge(
5561
                                GeneralUtility::intExplode(
5562
                                    ',',
5563
                                    $this->getTreeList(
5564
                                        $next_id,
5565
                                        $depth - 1,
5566
                                        $begin - 1,
5567
                                        $dontCheckEnableFields,
5568
                                        $addSelectFields,
5569
                                        $moreWhereClauses,
5570
                                        $prevId_array,
5571
                                        $recursionLevel + 1
5572
                                    ),
5573
                                    true
5574
                                ),
5575
                                $theList
5576
                            );
5577
                        }
5578
                    }
5579
                }
5580
            }
5581
        }
5582
        // If first run, check if the ID should be returned:
5583
        if (!$recursionLevel) {
5584
            if ($addId) {
5585
                if ($begin > 0) {
5586
                    $theList[] = 0;
5587
                } else {
5588
                    $theList[] = $addId;
5589
                }
5590
            }
5591
5592
            $cacheEntry = [
5593
                'md5hash' => $requestHash,
5594
                'pid' => $id,
5595
                'treelist' => implode(',', $theList),
5596
                'tstamp' => $GLOBALS['EXEC_TIME'],
5597
            ];
5598
5599
            // Only add to cache if not logged into TYPO3 Backend
5600
            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...
5601
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
5602
                try {
5603
                    $connection->transactional(function ($connection) use ($cacheEntry) {
5604
                        $connection->insert('cache_treelist', $cacheEntry);
5605
                    });
5606
                } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
5607
                }
5608
            }
5609
        }
5610
5611
        return implode(',', $theList);
5612
    }
5613
5614
    /**
5615
     * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.)
5616
     * 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%")'
5617
     *
5618
     * @param string $searchWords The search words. These will be separated by space and comma.
5619
     * @param string $searchFieldList The fields to search in
5620
     * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well)
5621
     * @return string The WHERE clause.
5622
     */
5623
    public function searchWhere($searchWords, $searchFieldList, $searchTable)
5624
    {
5625
        if (!$searchWords) {
5626
            return '';
5627
        }
5628
5629
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5630
            ->getQueryBuilderForTable($searchTable);
5631
5632
        $prefixTableName = $searchTable ? $searchTable . '.' : '';
5633
5634
        $where = $queryBuilder->expr()->andX();
5635
        $searchFields = explode(',', $searchFieldList);
5636
        $searchWords = preg_split('/[ ,]/', $searchWords);
5637
        foreach ($searchWords as $searchWord) {
5638
            $searchWord = trim($searchWord);
5639
            if (strlen($searchWord) < 3) {
5640
                continue;
5641
            }
5642
            $searchWordConstraint = $queryBuilder->expr()->orX();
5643
            $searchWord = $queryBuilder->escapeLikeWildcards($searchWord);
5644
            foreach ($searchFields as $field) {
5645
                $searchWordConstraint->add(
5646
                    $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%'))
5647
                );
5648
            }
5649
5650
            if ($searchWordConstraint->count()) {
5651
                $where->add($searchWordConstraint);
5652
            }
5653
        }
5654
5655
        if ((string)$where === '') {
5656
            return '';
5657
        }
5658
5659
        return ' AND (' . (string)$where . ')';
5660
    }
5661
5662
    /**
5663
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5664
     * This function is preferred over ->getQuery() if you just need to create and then execute a query.
5665
     *
5666
     * @param string $table The table name
5667
     * @param array $conf The TypoScript configuration properties
5668
     * @return Statement
5669
     * @see getQuery()
5670
     */
5671
    public function exec_getQuery($table, $conf)
5672
    {
5673
        $statement = $this->getQuery($table, $conf);
5674
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5675
5676
        return $connection->executeQuery($statement);
5677
    }
5678
5679
    /**
5680
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5681
     * and overlays with translation and version if available
5682
     *
5683
     * @param string $tableName the name of the TCA database table
5684
     * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference
5685
     * @return array The records
5686
     * @throws \UnexpectedValueException
5687
     */
5688
    public function getRecords($tableName, array $queryConfiguration)
5689
    {
5690
        $records = [];
5691
5692
        $statement = $this->exec_getQuery($tableName, $queryConfiguration);
5693
5694
        $tsfe = $this->getTypoScriptFrontendController();
5695
        while ($row = $statement->fetch()) {
5696
            // Versioning preview:
5697
            $tsfe->sys_page->versionOL($tableName, $row, true);
5698
5699
            // Language overlay:
5700
            if (is_array($row)) {
5701
                $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row);
5702
            }
5703
5704
            // Might be unset in the language overlay
5705
            if (is_array($row)) {
5706
                $records[] = $row;
5707
            }
5708
        }
5709
5710
        return $records;
5711
    }
5712
5713
    /**
5714
     * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array
5715
     * Implements the "select" function in TypoScript
5716
     *
5717
     * @param string $table See ->exec_getQuery()
5718
     * @param array $conf See ->exec_getQuery()
5719
     * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED!
5720
     * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts.
5721
     * @throws \RuntimeException
5722
     * @throws \InvalidArgumentException
5723
     * @internal
5724
     * @see numRows()
5725
     */
5726
    public function getQuery($table, $conf, $returnQueryArray = false)
5727
    {
5728
        // Resolve stdWrap in these properties first
5729
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5730
        $properties = [
5731
            'pidInList',
5732
            'uidInList',
5733
            'languageField',
5734
            'selectFields',
5735
            'max',
5736
            'begin',
5737
            'groupBy',
5738
            'orderBy',
5739
            'join',
5740
            'leftjoin',
5741
            'rightjoin',
5742
            'recursive',
5743
            'where'
5744
        ];
5745
        foreach ($properties as $property) {
5746
            $conf[$property] = trim(
5747
                isset($conf[$property . '.'])
0 ignored issues
show
Bug introduced by
It seems like IssetNode ? $this->stdWr...conf[$property] ?? null can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5747
                /** @scrutinizer ignore-type */ isset($conf[$property . '.'])
Loading history...
5748
                    ? $this->stdWrap($conf[$property] ?? '', $conf[$property . '.'] ?? [])
5749
                    : ($conf[$property] ?? null)
5750
            );
5751
            if ($conf[$property] === '') {
5752
                unset($conf[$property]);
5753
            } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) {
5754
                $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]);
5755
            }
5756
            if (isset($conf[$property . '.'])) {
5757
                // stdWrapping already done, so remove the sub-array
5758
                unset($conf[$property . '.']);
5759
            }
5760
        }
5761
        // Handle PDO-style named parameter markers first
5762
        $queryMarkers = $this->getQueryMarkers($table, $conf);
5763
        // Replace the markers in the non-stdWrap properties
5764
        foreach ($queryMarkers as $marker => $markerValue) {
5765
            $properties = [
5766
                'uidInList',
5767
                'selectFields',
5768
                'where',
5769
                'max',
5770
                'begin',
5771
                'groupBy',
5772
                'orderBy',
5773
                'join',
5774
                'leftjoin',
5775
                'rightjoin'
5776
            ];
5777
            foreach ($properties as $property) {
5778
                if ($conf[$property]) {
5779
                    $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]);
5780
                }
5781
            }
5782
        }
5783
5784
        // Construct WHERE clause:
5785
        // Handle recursive function for the pidInList
5786
        if (isset($conf['recursive'])) {
5787
            $conf['recursive'] = (int)$conf['recursive'];
5788
            if ($conf['recursive'] > 0) {
5789
                $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true);
5790
                array_walk($pidList, function (&$storagePid) {
5791
                    if ($storagePid === 'this') {
5792
                        $storagePid = $this->getTypoScriptFrontendController()->id;
5793
                    }
5794
                    if ($storagePid > 0) {
5795
                        $storagePid = -$storagePid;
5796
                    }
5797
                });
5798
                $expandedPidList = [];
5799
                foreach ($pidList as $value) {
5800
                    // Implementation of getTreeList allows to pass the id negative to include
5801
                    // it into the result otherwise only childpages are returned
5802
                    $expandedPidList = array_merge(
5803
                        GeneralUtility::intExplode(',', $this->getTreeList((int)$value, (int)($conf['recursive'] ?? 0))),
5804
                        $expandedPidList
5805
                    );
5806
                }
5807
                $conf['pidInList'] = implode(',', $expandedPidList);
5808
            }
5809
        }
5810
        if ((string)($conf['pidInList'] ?? '') === '') {
5811
            $conf['pidInList'] = 'this';
5812
        }
5813
5814
        $queryParts = $this->getQueryConstraints($table, $conf);
5815
5816
        $queryBuilder = $connection->createQueryBuilder();
5817
        // @todo Check against getQueryConstraints, can probably use FrontendRestrictions
5818
        // @todo here and remove enableFields there.
5819
        $queryBuilder->getRestrictions()->removeAll();
5820
        $queryBuilder->select('*')->from($table);
5821
5822
        if ($queryParts['where'] ?? false) {
5823
            $queryBuilder->where($queryParts['where']);
5824
        }
5825
5826
        if ($queryParts['groupBy'] ?? false) {
5827
            $queryBuilder->groupBy(...$queryParts['groupBy']);
5828
        }
5829
5830
        if (is_array($queryParts['orderBy'] ?? false)) {
5831
            foreach ($queryParts['orderBy'] as $orderBy) {
5832
                $queryBuilder->addOrderBy(...$orderBy);
5833
            }
5834
        }
5835
5836
        // Fields:
5837
        if ($conf['selectFields'] ?? false) {
5838
            $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table));
5839
        }
5840
5841
        // Setting LIMIT:
5842
        $error = false;
5843
        if (($conf['max'] ?? false) || ($conf['begin'] ?? false)) {
5844
            // Finding the total number of records, if used:
5845
            if (strpos(strtolower(($conf['begin'] ?? '') . $conf['max']), 'total') !== false) {
5846
                $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5847
                $countQueryBuilder->getRestrictions()->removeAll();
5848
                $countQueryBuilder->count('*')
5849
                    ->from($table)
5850
                    ->where($queryParts['where']);
5851
5852
                if ($queryParts['groupBy']) {
5853
                    $countQueryBuilder->groupBy(...$queryParts['groupBy']);
5854
                }
5855
5856
                try {
5857
                    $count = $countQueryBuilder->execute()->fetchOne();
5858
                    $conf['max'] = str_ireplace('total', $count, $conf['max']);
5859
                    $conf['begin'] = str_ireplace('total', $count, $conf['begin']);
5860
                } catch (DBALException $e) {
5861
                    $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage());
5862
                    $error = true;
5863
                }
5864
            }
5865
5866
            if (!$error) {
5867
                $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'] ?? '')), 0);
5868
                $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'] ?? '')), 0);
5869
                if ($conf['begin'] > 0) {
5870
                    $queryBuilder->setFirstResult($conf['begin']);
5871
                }
5872
                $queryBuilder->setMaxResults($conf['max'] ?: 100000);
5873
            }
5874
        }
5875
5876
        if (!$error) {
5877
            // Setting up tablejoins:
5878
            if ($conf['join'] ?? false) {
5879
                $joinParts = QueryHelper::parseJoin($conf['join']);
5880
                $queryBuilder->join(
5881
                    $table,
5882
                    $joinParts['tableName'],
5883
                    $joinParts['tableAlias'],
5884
                    $joinParts['joinCondition']
5885
                );
5886
            } elseif ($conf['leftjoin'] ?? false) {
5887
                $joinParts = QueryHelper::parseJoin($conf['leftjoin']);
5888
                $queryBuilder->leftJoin(
5889
                    $table,
5890
                    $joinParts['tableName'],
5891
                    $joinParts['tableAlias'],
5892
                    $joinParts['joinCondition']
5893
                );
5894
            } elseif ($conf['rightjoin'] ?? false) {
5895
                $joinParts = QueryHelper::parseJoin($conf['rightjoin']);
5896
                $queryBuilder->rightJoin(
5897
                    $table,
5898
                    $joinParts['tableName'],
5899
                    $joinParts['tableAlias'],
5900
                    $joinParts['joinCondition']
5901
                );
5902
            }
5903
5904
            // Convert the QueryBuilder object into a SQL statement.
5905
            $query = $queryBuilder->getSQL();
5906
5907
            // Replace the markers in the queryParts to handle stdWrap enabled properties
5908
            foreach ($queryMarkers as $marker => $markerValue) {
5909
                // @todo Ugly hack that needs to be cleaned up, with the current architecture
5910
                // @todo for exec_Query / getQuery it's the best we can do.
5911
                $query = str_replace('###' . $marker . '###', $markerValue, $query);
5912
                foreach ($queryParts as $queryPartKey => &$queryPartValue) {
5913
                    $queryPartValue = str_replace('###' . $marker . '###', $markerValue, $queryPartValue);
5914
                }
5915
                unset($queryPartValue);
5916
            }
5917
5918
            return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
5919
        }
5920
5921
        return '';
5922
    }
5923
5924
    /**
5925
     * Helper to transform a QueryBuilder object into a queryParts array that can be used
5926
     * with exec_SELECT_queryArray
5927
     *
5928
     * @param \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder
5929
     * @return array
5930
     * @throws \RuntimeException
5931
     */
5932
    protected function getQueryArray(QueryBuilder $queryBuilder)
5933
    {
5934
        $fromClauses = [];
5935
        $knownAliases = [];
5936
        $queryParts = [];
5937
5938
        // Loop through all FROM clauses
5939
        foreach ($queryBuilder->getQueryPart('from') as $from) {
5940
            if ($from['alias'] === null) {
5941
                $tableSql = $from['table'];
5942
                $tableReference = $from['table'];
5943
            } else {
5944
                $tableSql = $from['table'] . ' ' . $from['alias'];
5945
                $tableReference = $from['alias'];
5946
            }
5947
5948
            $knownAliases[$tableReference] = true;
5949
5950
            $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
5951
                $tableReference,
5952
                $queryBuilder->getQueryPart('join'),
5953
                $knownAliases
5954
            );
5955
        }
5956
5957
        $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
5958
        $queryParts['FROM'] = implode(', ', $fromClauses);
5959
        $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
5960
        $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
5961
        $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
5962
        if ($queryBuilder->getFirstResult() > 0) {
5963
            $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
5964
        } elseif ($queryBuilder->getMaxResults() > 0) {
5965
            $queryParts['LIMIT'] = $queryBuilder->getMaxResults();
5966
        }
5967
5968
        return $queryParts;
5969
    }
5970
5971
    /**
5972
     * Helper to transform the QueryBuilder join part into a SQL fragment.
5973
     *
5974
     * @param string $fromAlias
5975
     * @param array $joinParts
5976
     * @param array $knownAliases
5977
     * @return string
5978
     * @throws \RuntimeException
5979
     */
5980
    protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
5981
    {
5982
        $sql = '';
5983
5984
        if (isset($joinParts['join'][$fromAlias])) {
5985
            foreach ($joinParts['join'][$fromAlias] as $join) {
5986
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
5987
                    throw new \RuntimeException(
5988
                        'Non unique join alias: "' . $join['joinAlias'] . '" found.',
5989
                        1472748872
5990
                    );
5991
                }
5992
                $sql .= ' ' . strtoupper($join['joinType'])
5993
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
5994
                    . ' ON ' . ((string)$join['joinCondition']);
5995
                $knownAliases[$join['joinAlias']] = true;
5996
            }
5997
5998
            foreach ($joinParts['join'][$fromAlias] as $join) {
5999
                $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
6000
            }
6001
        }
6002
6003
        return $sql;
6004
    }
6005
    /**
6006
     * Helper function for getQuery(), creating the WHERE clause of the SELECT query
6007
     *
6008
     * @param string $table The table name
6009
     * @param array $conf The TypoScript configuration properties
6010
     * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments
6011
     * @throws \InvalidArgumentException
6012
     * @see getQuery()
6013
     */
6014
    protected function getQueryConstraints(string $table, array $conf): array
6015
    {
6016
        // Init:
6017
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6018
        $expressionBuilder = $queryBuilder->expr();
6019
        $tsfe = $this->getTypoScriptFrontendController();
6020
        $constraints = [];
6021
        $pid_uid_flag = 0;
6022
        $enableFieldsIgnore = [];
6023
        $queryParts = [
6024
            'where' => null,
6025
            'groupBy' => null,
6026
            'orderBy' => null,
6027
        ];
6028
6029
        $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
6030
        $considerMovePointers = (
6031
            $isInWorkspace && $table !== 'pages'
6032
            && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
6033
        );
6034
6035
        if (trim($conf['uidInList'] ?? '')) {
6036
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['uidInList']));
6037
6038
            // If moved records shall be considered, select via t3ver_oid
6039
            if ($considerMovePointers) {
6040
                $constraints[] = (string)$expressionBuilder->orX(
6041
                    $expressionBuilder->in($table . '.uid', $listArr),
6042
                    $expressionBuilder->andX(
6043
                        $expressionBuilder->eq(
6044
                            $table . '.t3ver_state',
6045
                            (int)(string)VersionState::cast(VersionState::MOVE_POINTER)
6046
                        ),
6047
                        $expressionBuilder->in($table . '.t3ver_oid', $listArr)
6048
                    )
6049
                );
6050
            } else {
6051
                $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr);
6052
            }
6053
            $pid_uid_flag++;
6054
        }
6055
6056
        // Static_* tables are allowed to be fetched from root page
6057
        if (strpos($table, 'static_') === 0) {
6058
            $pid_uid_flag++;
6059
        }
6060
6061
        if (trim($conf['pidInList'])) {
6062
            $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['pidInList']));
6063
            // Removes all pages which are not visible for the user!
6064
            $listArr = $this->checkPidArray($listArr);
6065
            if (GeneralUtility::inList($conf['pidInList'], 'root')) {
6066
                $listArr[] = 0;
6067
            }
6068
            if (GeneralUtility::inList($conf['pidInList'], '-1')) {
6069
                $listArr[] = -1;
6070
                $enableFieldsIgnore['pid'] = true;
6071
            }
6072
            if (!empty($listArr)) {
6073
                $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr));
6074
                $pid_uid_flag++;
6075
            } else {
6076
                // If not uid and not pid then uid is set to 0 - which results in nothing!!
6077
                $pid_uid_flag = 0;
6078
            }
6079
        }
6080
6081
        // If not uid and not pid then uid is set to 0 - which results in nothing!!
6082
        if (!$pid_uid_flag) {
6083
            $constraints[] = $expressionBuilder->eq($table . '.uid', 0);
6084
        }
6085
6086
        $where = trim((string)$this->stdWrapValue('where', $conf ?? []));
6087
        if ($where) {
6088
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
6089
        }
6090
6091
        // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched
6092
        // but only do this for TCA tables that have languages enabled
6093
        $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class));
6094
        if ($languageConstraint !== null) {
6095
            $constraints[] = $languageConstraint;
6096
        }
6097
6098
        // Enablefields
6099
        if ($table === 'pages') {
6100
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
6101
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
6102
        } else {
6103
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
6104
        }
6105
6106
        // MAKE WHERE:
6107
        if (count($constraints) !== 0) {
6108
            $queryParts['where'] = $expressionBuilder->andX(...$constraints);
6109
        }
6110
        // GROUP BY
6111
        $groupBy = trim((string)$this->stdWrapValue('groupBy', $conf ?? []));
6112
        if ($groupBy) {
6113
            $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy);
6114
        }
6115
6116
        // ORDER BY
6117
        $orderByString = trim((string)$this->stdWrapValue('orderBy', $conf ?? []));
6118
        if ($orderByString) {
6119
            $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString);
6120
        }
6121
6122
        // Return result:
6123
        return $queryParts;
6124
    }
6125
6126
    /**
6127
     * Adds parts to the WHERE clause that are related to language.
6128
     * This only works on TCA tables which have the [ctrl][languageField] field set or if they
6129
     * have select.languageField = my_language_field set explicitly.
6130
     *
6131
     * It is also possible to disable the language restriction for a query by using select.languageField = 0,
6132
     * if select.languageField is not explicitly set, the TCA default values are taken.
6133
     *
6134
     * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted:
6135
     *
6136
     * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are
6137
     * fetched (the overlays are taken care of later-on).
6138
     * if the current language has overlays but also records without localization-parent (free mode) available,
6139
     * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1
6140
     * which overrules the overlayType within the language aspect.
6141
     *
6142
     * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records
6143
     * for the current language.
6144
     *
6145
     * @param ExpressionBuilder $expressionBuilder
6146
     * @param string $table
6147
     * @param array $conf
6148
     * @param Context $context
6149
     * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null
6150
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
6151
     */
6152
    protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
6153
    {
6154
        $languageField = '';
6155
        $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
6156
        // Check if the table is translatable, and set the language field by default from the TCA information
6157
        if (!empty($conf['languageField']) || !isset($conf['languageField'])) {
6158
            if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) {
6159
                $languageField = $conf['languageField'];
6160
            } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) {
6161
                $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
6162
            }
6163
        }
6164
6165
        // No language restriction enabled explicitly or available via TCA
6166
        if (empty($languageField)) {
6167
            return null;
6168
        }
6169
6170
        /** @var LanguageAspect $languageAspect */
6171
        $languageAspect = $context->getAspect('language');
6172
        if ($languageAspect->doOverlays() && !empty($localizationParentField)) {
6173
            // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will
6174
            // OVERLAY the records with localized versions!
6175
            $languageQuery = $expressionBuilder->in($languageField, [0, -1]);
6176
            // Use this option to include records that don't have a default language counterpart ("free mode")
6177
            // (originalpointerfield is 0 and the language field contains the requested language)
6178
            if (isset($conf['includeRecordsWithoutDefaultTranslation']) || !empty($conf['includeRecordsWithoutDefaultTranslation.'])) {
6179
                $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.'])
6180
                    ? $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.'])
6181
                    : $conf['includeRecordsWithoutDefaultTranslation'];
6182
                $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== '';
6183
            } else {
6184
                // Option was not explicitly set, check what's in for the language overlay type.
6185
                $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING;
6186
            }
6187
            if ($includeRecordsWithoutDefaultTranslation) {
6188
                $languageQuery = $expressionBuilder->orX(
6189
                    $languageQuery,
6190
                    $expressionBuilder->andX(
6191
                        $expressionBuilder->eq($table . '.' . $localizationParentField, 0),
6192
                        $expressionBuilder->eq($languageField, $languageAspect->getContentId())
6193
                    )
6194
                );
6195
            }
6196
            return $languageQuery;
6197
        }
6198
        // No overlays = only fetch records given for the requested language and "all languages"
6199
        return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]);
6200
    }
6201
6202
    /**
6203
     * Helper function for getQuery, sanitizing the select part
6204
     *
6205
     * This functions checks if the necessary fields are part of the select
6206
     * and adds them if necessary.
6207
     *
6208
     * @param string $selectPart Select part
6209
     * @param string $table Table to select from
6210
     * @return string Sanitized select part
6211
     * @internal
6212
     * @see getQuery
6213
     */
6214
    protected function sanitizeSelectPart($selectPart, $table)
6215
    {
6216
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6217
6218
        // Pattern matching parts
6219
        $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
6220
        $matchEnd = '(\\s*,|\\s*$)/';
6221
        $necessaryFields = ['uid', 'pid'];
6222
        $wsFields = ['t3ver_state'];
6223
        if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) {
6224
            foreach ($necessaryFields as $field) {
6225
                $match = $matchStart . $field . $matchEnd;
6226
                if (!preg_match($match, $selectPart)) {
6227
                    $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6228
                }
6229
            }
6230
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
6231
                foreach ($wsFields 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
            }
6238
        }
6239
        return $selectPart;
6240
    }
6241
6242
    /**
6243
     * 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)
6244
     *
6245
     * @param int[] $pageIds Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed.
6246
     * @return array Returns the array of remaining page UID numbers
6247
     * @internal
6248
     */
6249
    public function checkPidArray($pageIds)
6250
    {
6251
        if (!is_array($pageIds) || empty($pageIds)) {
0 ignored issues
show
introduced by
The condition is_array($pageIds) is always true.
Loading history...
6252
            return [];
6253
        }
6254
        $restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class);
6255
        $restrictionContainer->add(GeneralUtility::makeInstance(
6256
            DocumentTypeExclusionRestriction::class,
6257
            GeneralUtility::intExplode(',', (string)$this->checkPid_badDoktypeList, true)
6258
        ));
6259
        return $this->getTypoScriptFrontendController()->sys_page->filterAccessiblePageIds($pageIds, $restrictionContainer);
6260
    }
6261
6262
    /**
6263
     * Builds list of marker values for handling PDO-like parameter markers in select parts.
6264
     * 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.
6265
     *
6266
     * @param string $table Table to select records from
6267
     * @param array $conf Select part of CONTENT definition
6268
     * @return array List of values to replace markers with
6269
     * @internal
6270
     * @see getQuery()
6271
     */
6272
    public function getQueryMarkers($table, $conf)
6273
    {
6274
        if (!isset($conf['markers.']) || !is_array($conf['markers.'])) {
6275
            return [];
6276
        }
6277
        // Parse markers and prepare their values
6278
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6279
        $markerValues = [];
6280
        foreach ($conf['markers.'] as $dottedMarker => $dummy) {
6281
            $marker = rtrim($dottedMarker, '.');
6282
            if ($dottedMarker != $marker . '.') {
6283
                continue;
6284
            }
6285
            // Parse definition
6286
            // todo else value is always null
6287
            $tempValue = isset($conf['markers.'][$dottedMarker])
6288
                ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'], $conf['markers.'][$dottedMarker])
6289
                : $conf['markers.'][$dottedMarker]['value'];
6290
            // Quote/escape if needed
6291
            if (is_numeric($tempValue)) {
6292
                if ((int)$tempValue == $tempValue) {
6293
                    // Handle integer
6294
                    $markerValues[$marker] = (int)$tempValue;
6295
                } else {
6296
                    // Handle float
6297
                    $markerValues[$marker] = (float)$tempValue;
6298
                }
6299
            } elseif ($tempValue === null) {
6300
                // It represents NULL
6301
                $markerValues[$marker] = 'NULL';
6302
            } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
6303
                // See if it is really a comma separated list of values
6304
                $explodeValues = GeneralUtility::trimExplode(',', $tempValue);
6305
                if (count($explodeValues) > 1) {
6306
                    // Handle each element of list separately
6307
                    $tempArray = [];
6308
                    foreach ($explodeValues as $listValue) {
6309
                        if (is_numeric($listValue)) {
6310
                            if ((int)$listValue == $listValue) {
6311
                                $tempArray[] = (int)$listValue;
6312
                            } else {
6313
                                $tempArray[] = (float)$listValue;
6314
                            }
6315
                        } else {
6316
                            // If quoted, remove quotes before
6317
                            // escaping.
6318
                            if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
6319
                                $listValue = $matches[1];
6320
                            } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
6321
                                $listValue = $matches[1];
6322
                            }
6323
                            $tempArray[] = $connection->quote($listValue);
6324
                        }
6325
                    }
6326
                    $markerValues[$marker] = implode(',', $tempArray);
6327
                } else {
6328
                    // Handle remaining values as string
6329
                    $markerValues[$marker] = $connection->quote($tempValue);
6330
                }
6331
            } else {
6332
                // Handle remaining values as string
6333
                $markerValues[$marker] = $connection->quote($tempValue);
6334
            }
6335
        }
6336
        return $markerValues;
6337
    }
6338
6339
    /***********************************************
6340
     *
6341
     * Frontend editing functions
6342
     *
6343
     ***********************************************/
6344
    /**
6345
     * 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.
6346
     * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
6347
     * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
6348
     *
6349
     * @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.
6350
     * @param array $conf TypoScript configuration properties for the editPanel
6351
     * @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
6352
     * @param array $dataArray Alternative data array to use. Default is $this->data
6353
     * @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.
6354
     */
6355
    public function editPanel($content, $conf, $currentRecord = '', $dataArray = [])
6356
    {
6357
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6358
            return $content;
6359
        }
6360
        if (!$this->getTypoScriptFrontendController()->displayEditIcons) {
6361
            return $content;
6362
        }
6363
6364
        if (!$currentRecord) {
6365
            $currentRecord = $this->currentRecord;
6366
        }
6367
        if (empty($dataArray)) {
6368
            $dataArray = $this->data;
6369
        }
6370
6371
        if ($conf['newRecordFromTable']) {
6372
            $currentRecord = $conf['newRecordFromTable'] . ':NEW';
6373
            $conf['allow'] = 'new';
6374
            $checkEditAccessInternals = false;
6375
        } else {
6376
            $checkEditAccessInternals = true;
6377
        }
6378
        [$table, $uid] = explode(':', $currentRecord);
6379
        // Page ID for new records, 0 if not specified
6380
        $newRecordPid = (int)$conf['newRecordInPid'];
6381
        $newUid = null;
6382
        if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) {
6383
            if ($table === 'pages') {
6384
                $newUid = $uid;
6385
            } else {
6386
                if ($conf['newRecordFromTable']) {
6387
                    $newUid = $this->getTypoScriptFrontendController()->id;
6388
                    if ($newRecordPid) {
6389
                        $newUid = $newRecordPid;
6390
                    }
6391
                } else {
6392
                    $newUid = -1 * $uid;
6393
                }
6394
            }
6395
        }
6396
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6397
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6398
            if ($editClass) {
6399
                $edit = GeneralUtility::makeInstance($editClass);
6400
                $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']);
6401
                $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []);
6402
            }
6403
        }
6404
        return $content;
6405
    }
6406
6407
    /**
6408
     * 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.
6409
     * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
6410
     *
6411
     * @param string $content The content to which the edit icons should be appended
6412
     * @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
6413
     * @param array $conf TypoScript properties for configuring the edit icons.
6414
     * @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
6415
     * @param array $dataArray Alternative data array to use. Default is $this->data
6416
     * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine
6417
     * @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.
6418
     */
6419
    public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '')
6420
    {
6421
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6422
            return $content;
6423
        }
6424
        if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) {
6425
            return $content;
6426
        }
6427
        if (!$currentRecord) {
6428
            $currentRecord = $this->currentRecord;
6429
        }
6430
        if (empty($dataArray)) {
6431
            $dataArray = $this->data;
6432
        }
6433
        // Check incoming params:
6434
        [$currentRecordTable, $currentRecordUID] = explode(':', $currentRecord);
6435
        [$fieldList, $table] = array_reverse(GeneralUtility::trimExplode(':', $params, true));
6436
        // Reverse the array because table is optional
6437
        if (!$table) {
6438
            $table = $currentRecordTable;
6439
        } elseif ($table != $currentRecordTable) {
6440
            // If the table is set as the first parameter, and does not match the table of the current record, then just return.
6441
            return $content;
6442
        }
6443
6444
        $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID;
6445
        // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
6446
        if (!array_key_exists('allow', $conf)) {
6447
            $conf['allow'] = 'edit';
6448
        }
6449
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6450
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6451
            if ($editClass) {
6452
                $edit = GeneralUtility::makeInstance($editClass);
6453
                $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
6454
            }
6455
        }
6456
        return $content;
6457
    }
6458
6459
    /**
6460
     * Returns TRUE if the input table/row would be hidden in the frontend (according nto the current time and simulate user group)
6461
     *
6462
     * @param string $table The table name
6463
     * @param array $row The data record
6464
     * @return bool
6465
     * @internal
6466
     * @see editPanelPreviewBorder()
6467
     */
6468
    public function isDisabled($table, $row)
6469
    {
6470
        $tsfe = $this->getTypoScriptFrontendController();
6471
        $enablecolumns = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
6472
        return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']]
6473
            || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup
6474
            || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > $GLOBALS['EXEC_TIME']
6475
            || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < $GLOBALS['EXEC_TIME'];
6476
    }
6477
6478
    /**
6479
     * Get instance of FAL resource factory
6480
     *
6481
     * @return ResourceFactory
6482
     */
6483
    protected function getResourceFactory()
6484
    {
6485
        return GeneralUtility::makeInstance(ResourceFactory::class);
6486
    }
6487
6488
    /**
6489
     * Wrapper function for GeneralUtility::getIndpEnv()
6490
     *
6491
     * @see GeneralUtility::getIndpEnv
6492
     * @param string $key Name of the "environment variable"/"server variable" you wish to get.
6493
     * @return string
6494
     */
6495
    protected function getEnvironmentVariable($key)
6496
    {
6497
        if ($key === 'REQUEST_URI') {
6498
            return $this->getRequest()->getAttribute('normalizedParams')->getRequestUri();
6499
        }
6500
        return GeneralUtility::getIndpEnv($key);
6501
    }
6502
6503
    /**
6504
     * Fetches content from cache
6505
     *
6506
     * @param array $configuration Array
6507
     * @return string|bool FALSE on cache miss
6508
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
6509
     */
6510
    protected function getFromCache(array $configuration)
6511
    {
6512
        $content = false;
6513
6514
        if ($this->getTypoScriptFrontendController()->no_cache) {
6515
            return $content;
6516
        }
6517
        $cacheKey = $this->calculateCacheKey($configuration);
6518
        if (!empty($cacheKey)) {
6519
            /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
6520
            $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)
6521
                ->getCache('hash');
6522
            $content = $cacheFrontend->get($cacheKey);
6523
        }
6524
        return $content;
6525
    }
6526
6527
    /**
6528
     * Calculates the lifetime of a cache entry based on the given configuration
6529
     *
6530
     * @param array $configuration
6531
     * @return int|null
6532
     */
6533
    protected function calculateCacheLifetime(array $configuration)
6534
    {
6535
        $configuration['lifetime'] = $configuration['lifetime'] ?? '';
6536
        $lifetimeConfiguration = (string)$this->stdWrapValue('lifetime', $configuration);
6537
6538
        $lifetime = null; // default lifetime
6539
        if (strtolower($lifetimeConfiguration) === 'unlimited') {
6540
            $lifetime = 0; // unlimited
6541
        } elseif ($lifetimeConfiguration > 0) {
6542
            $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds
6543
        }
6544
        return $lifetime;
6545
    }
6546
6547
    /**
6548
     * Calculates the tags for a cache entry bases on the given configuration
6549
     *
6550
     * @param array $configuration
6551
     * @return array
6552
     */
6553
    protected function calculateCacheTags(array $configuration)
6554
    {
6555
        $configuration['tags'] = $configuration['tags'] ?? '';
6556
        $tags = (string)$this->stdWrapValue('tags', $configuration);
6557
        return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags);
6558
    }
6559
6560
    /**
6561
     * Applies stdWrap to the cache key
6562
     *
6563
     * @param array $configuration
6564
     * @return string
6565
     */
6566
    protected function calculateCacheKey(array $configuration)
6567
    {
6568
        $configuration['key'] = $configuration['key'] ?? '';
6569
        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...
6570
    }
6571
6572
    /**
6573
     * Returns the current BE user.
6574
     *
6575
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
6576
     */
6577
    protected function getFrontendBackendUser()
6578
    {
6579
        return $GLOBALS['BE_USER'];
6580
    }
6581
6582
    /**
6583
     * @return TimeTracker
6584
     */
6585
    protected function getTimeTracker()
6586
    {
6587
        return GeneralUtility::makeInstance(TimeTracker::class);
6588
    }
6589
6590
    /**
6591
     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
6592
     */
6593
    protected function getTypoScriptFrontendController()
6594
    {
6595
        return $this->typoScriptFrontendController ?: $GLOBALS['TSFE'] ?? null;
6596
    }
6597
6598
    /**
6599
     * Support anchors without href value
6600
     * Changes ContentObjectRenderer::typolink to render a tag without href,
6601
     * if id or name attribute is present.
6602
     *
6603
     * @param string $linkText
6604
     * @param array $conf Typolink configuration decoded as array
6605
     * @return string Full a-Tag or just the linktext if id or name are not set.
6606
     */
6607
    protected function resolveAnchorLink(string $linkText, array $conf): string
6608
    {
6609
        $anchorTag = '<a ' . $this->getATagParams($conf) . '>';
6610
        $aTagParams = GeneralUtility::get_tag_attributes($anchorTag);
6611
        // If it looks like a anchor tag, render it anyway
6612
        if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
6613
            return $anchorTag . $linkText . '</a>';
6614
        }
6615
        // Otherwise just return the link text
6616
        return $linkText;
6617
    }
6618
6619
    /**
6620
     * Get content length of the current tag that could also contain nested tag contents
6621
     *
6622
     * @param string $theValue
6623
     * @param int $pointer
6624
     * @param string $currentTag
6625
     * @return int
6626
     */
6627
    protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int
6628
    {
6629
        $tempContent = strtolower(substr($theValue, $pointer));
6630
        $startTag = '<' . $currentTag;
6631
        $endTag = '</' . $currentTag . '>';
6632
        $offsetCount = 0;
6633
6634
        // Take care for nested tags
6635
        do {
6636
            $nextMatchingEndTagPosition = strpos($tempContent, $endTag);
6637
            // only match tag `a` in `<a href"...">` but not in `<abbr>`
6638
            $nextSameTypeTagPosition = preg_match(
6639
                '#' . $startTag . '[\s/>]#',
6640
                $tempContent,
6641
                $nextSameStartTagMatches,
6642
                PREG_OFFSET_CAPTURE
6643
            ) ? $nextSameStartTagMatches[0][1] : false;
6644
6645
            // filter out nested tag contents to help getting the correct closing tag
6646
            if ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) {
6647
                $lastOpeningTagStartPosition = (int)strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag);
6648
                $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag);
6649
                $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition;
6650
6651
                // replace content from latest tag start to latest tag end
6652
                $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition);
6653
            }
6654
        } while (
6655
            ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) &&
6656
            $nextSameTypeTagPosition < $nextMatchingEndTagPosition
6657
        );
6658
6659
        // if no closing tag is found we use length of the whole content
6660
        $endingOffset = strlen($tempContent);
6661
        if ($nextMatchingEndTagPosition !== false) {
6662
            $endingOffset = $nextMatchingEndTagPosition + $offsetCount;
6663
        }
6664
6665
        return $endingOffset;
6666
    }
6667
6668
    public function getRequest(): ServerRequestInterface
6669
    {
6670
        if ($this->request instanceof ServerRequestInterface) {
6671
            return $this->request;
6672
        }
6673
6674
        if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
6675
            return $GLOBALS['TYPO3_REQUEST'];
6676
        }
6677
6678
        throw new ContentRenderingException('PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972);
6679
    }
6680
}
6681