1<?php
2namespace TYPO3\CMS\Frontend\ContentObject;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use Doctrine\DBAL\DBALException;
18use Doctrine\DBAL\Driver\Statement;
19use Psr\Log\LoggerAwareInterface;
20use Psr\Log\LoggerAwareTrait;
21use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
22use TYPO3\CMS\Core\Cache\CacheManager;
23use TYPO3\CMS\Core\Configuration\Features;
24use TYPO3\CMS\Core\Context\Context;
25use TYPO3\CMS\Core\Context\LanguageAspect;
26use TYPO3\CMS\Core\Core\Environment;
27use TYPO3\CMS\Core\Database\Connection;
28use TYPO3\CMS\Core\Database\ConnectionPool;
29use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
30use TYPO3\CMS\Core\Database\Query\QueryBuilder;
31use TYPO3\CMS\Core\Database\Query\QueryHelper;
32use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
33use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
34use TYPO3\CMS\Core\Html\HtmlParser;
35use TYPO3\CMS\Core\Html\SanitizerBuilderFactory;
36use TYPO3\CMS\Core\Html\SanitizerInitiator;
37use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
38use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
39use TYPO3\CMS\Core\LinkHandling\LinkService;
40use TYPO3\CMS\Core\Log\LogManager;
41use TYPO3\CMS\Core\Mail\MailMessage;
42use TYPO3\CMS\Core\Resource\Exception;
43use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
44use TYPO3\CMS\Core\Resource\File;
45use TYPO3\CMS\Core\Resource\FileInterface;
46use TYPO3\CMS\Core\Resource\FileReference;
47use TYPO3\CMS\Core\Resource\ProcessedFile;
48use TYPO3\CMS\Core\Resource\ResourceFactory;
49use TYPO3\CMS\Core\Resource\StorageRepository;
50use TYPO3\CMS\Core\Service\DependencyOrderingService;
51use TYPO3\CMS\Core\Service\FlexFormService;
52use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
53use TYPO3\CMS\Core\Site\Entity\Site;
54use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
55use TYPO3\CMS\Core\TimeTracker\TimeTracker;
56use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
57use TYPO3\CMS\Core\TypoScript\TypoScriptService;
58use TYPO3\CMS\Core\Utility\ArrayUtility;
59use TYPO3\CMS\Core\Utility\DebugUtility;
60use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
61use TYPO3\CMS\Core\Utility\GeneralUtility;
62use TYPO3\CMS\Core\Utility\HttpUtility;
63use TYPO3\CMS\Core\Utility\MailUtility;
64use TYPO3\CMS\Core\Utility\MathUtility;
65use TYPO3\CMS\Core\Utility\PathUtility;
66use TYPO3\CMS\Core\Utility\StringUtility;
67use TYPO3\CMS\Core\Versioning\VersionState;
68use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
69use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
70use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
71use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
72use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
73use TYPO3\CMS\Frontend\Imaging\GifBuilder;
74use TYPO3\CMS\Frontend\Page\PageRepository;
75use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
76use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
77use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
78use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
79use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
80
81/**
82 * This class contains all main TypoScript features.
83 * This includes the rendering of TypoScript content objects (cObjects).
84 * Is the backbone of TypoScript Template rendering.
85 *
86 * There are lots of functions you can use from your include-scripts.
87 * The class is normally instantiated and referred to as "cObj".
88 * 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.
89 */
90class ContentObjectRenderer implements LoggerAwareInterface
91{
92    use LoggerAwareTrait;
93
94    /**
95     * @var array
96     */
97    public $align = [
98        'center',
99        'right',
100        'left'
101    ];
102
103    /**
104     * stdWrap functions in their correct order
105     *
106     * @see stdWrap()
107     */
108    public $stdWrapOrder = [
109        'stdWrapPreProcess' => 'hook',
110        // this is a placeholder for the first Hook
111        'cacheRead' => 'hook',
112        // this is a placeholder for checking if the content is available in cache
113        'setContentToCurrent' => 'boolean',
114        'setContentToCurrent.' => 'array',
115        'addPageCacheTags' => 'string',
116        'addPageCacheTags.' => 'array',
117        'setCurrent' => 'string',
118        'setCurrent.' => 'array',
119        'lang.' => 'array',
120        'data' => 'getText',
121        'data.' => 'array',
122        'field' => 'fieldName',
123        'field.' => 'array',
124        'current' => 'boolean',
125        'current.' => 'array',
126        'cObject' => 'cObject',
127        'cObject.' => 'array',
128        'numRows.' => 'array',
129        // @deprecated - will be removed in TYPO3 v10.0.
130        'filelist' => 'dir',
131        // @deprecated - will be removed in TYPO3 v10.0.
132        'filelist.' => 'array',
133        'preUserFunc' => 'functionName',
134        'stdWrapOverride' => 'hook',
135        // this is a placeholder for the second Hook
136        'override' => 'string',
137        'override.' => 'array',
138        'preIfEmptyListNum' => 'listNum',
139        'preIfEmptyListNum.' => 'array',
140        'ifNull' => 'string',
141        'ifNull.' => 'array',
142        'ifEmpty' => 'string',
143        'ifEmpty.' => 'array',
144        'ifBlank' => 'string',
145        'ifBlank.' => 'array',
146        'listNum' => 'listNum',
147        'listNum.' => 'array',
148        'trim' => 'boolean',
149        'trim.' => 'array',
150        'strPad.' => 'array',
151        'stdWrap' => 'stdWrap',
152        'stdWrap.' => 'array',
153        'stdWrapProcess' => 'hook',
154        // this is a placeholder for the third Hook
155        'required' => 'boolean',
156        'required.' => 'array',
157        'if.' => 'array',
158        'fieldRequired' => 'fieldName',
159        'fieldRequired.' => 'array',
160        'csConv' => 'string',
161        'csConv.' => 'array',
162        'parseFunc' => 'objectpath',
163        'parseFunc.' => 'array',
164        'HTMLparser' => 'boolean',
165        'HTMLparser.' => 'array',
166        'split.' => 'array',
167        'replacement.' => 'array',
168        'prioriCalc' => 'boolean',
169        'prioriCalc.' => 'array',
170        'char' => 'integer',
171        'char.' => 'array',
172        'intval' => 'boolean',
173        'intval.' => 'array',
174        'hash' => 'string',
175        'hash.' => 'array',
176        'round' => 'boolean',
177        'round.' => 'array',
178        'numberFormat.' => 'array',
179        'expandList' => 'boolean',
180        'expandList.' => 'array',
181        'date' => 'dateconf',
182        'date.' => 'array',
183        'strtotime' => 'strtotimeconf',
184        'strtotime.' => 'array',
185        'strftime' => 'strftimeconf',
186        'strftime.' => 'array',
187        'age' => 'boolean',
188        'age.' => 'array',
189        'case' => 'case',
190        'case.' => 'array',
191        'bytes' => 'boolean',
192        'bytes.' => 'array',
193        'substring' => 'parameters',
194        'substring.' => 'array',
195        'cropHTML' => 'crop',
196        'cropHTML.' => 'array',
197        'stripHtml' => 'boolean',
198        'stripHtml.' => 'array',
199        'crop' => 'crop',
200        'crop.' => 'array',
201        'rawUrlEncode' => 'boolean',
202        'rawUrlEncode.' => 'array',
203        'htmlSpecialChars' => 'boolean',
204        'htmlSpecialChars.' => 'array',
205        'encodeForJavaScriptValue' => 'boolean',
206        'encodeForJavaScriptValue.' => 'array',
207        'doubleBrTag' => 'string',
208        'doubleBrTag.' => 'array',
209        'br' => 'boolean',
210        'br.' => 'array',
211        'brTag' => 'string',
212        'brTag.' => 'array',
213        'encapsLines.' => 'array',
214        'keywords' => 'boolean',
215        'keywords.' => 'array',
216        'innerWrap' => 'wrap',
217        'innerWrap.' => 'array',
218        'innerWrap2' => 'wrap',
219        'innerWrap2.' => 'array',
220        // @deprecated - will be removed in TYPO3 v10.0.
221        'addParams.' => 'array',
222        // @deprecated - will be removed in TYPO3 v10.0.
223        'filelink.' => 'array',
224        'preCObject' => 'cObject',
225        'preCObject.' => 'array',
226        'postCObject' => 'cObject',
227        'postCObject.' => 'array',
228        'wrapAlign' => 'align',
229        'wrapAlign.' => 'array',
230        'typolink.' => 'array',
231        'wrap' => 'wrap',
232        'wrap.' => 'array',
233        'noTrimWrap' => 'wrap',
234        'noTrimWrap.' => 'array',
235        'wrap2' => 'wrap',
236        'wrap2.' => 'array',
237        'dataWrap' => 'dataWrap',
238        'dataWrap.' => 'array',
239        'prepend' => 'cObject',
240        'prepend.' => 'array',
241        'append' => 'cObject',
242        'append.' => 'array',
243        'wrap3' => 'wrap',
244        'wrap3.' => 'array',
245        'orderedStdWrap' => 'stdWrap',
246        'orderedStdWrap.' => 'array',
247        'outerWrap' => 'wrap',
248        'outerWrap.' => 'array',
249        'insertData' => 'boolean',
250        'insertData.' => 'array',
251        'postUserFunc' => 'functionName',
252        'postUserFuncInt' => 'functionName',
253        'prefixComment' => 'string',
254        'prefixComment.' => 'array',
255        'editIcons' => 'string',
256        'editIcons.' => 'array',
257        'editPanel' => 'boolean',
258        'editPanel.' => 'array',
259        'htmlSanitize' => 'boolean',
260        'htmlSanitize.' => 'array',
261        'cacheStore' => 'hook',
262        // this is a placeholder for storing the content in cache
263        'stdWrapPostProcess' => 'hook',
264        // this is a placeholder for the last Hook
265        'debug' => 'boolean',
266        'debug.' => 'array',
267        'debugFunc' => 'boolean',
268        'debugFunc.' => 'array',
269        'debugData' => 'boolean',
270        'debugData.' => 'array'
271    ];
272
273    /**
274     * Class names for accordant content object names
275     *
276     * @var array
277     */
278    protected $contentObjectClassMap = [];
279
280    /**
281     * Loaded with the current data-record.
282     *
283     * If the instance of this class is used to render records from the database those records are found in this array.
284     * The function stdWrap has TypoScript properties that fetch field-data from this array.
285     *
286     * @var array
287     * @see start()
288     */
289    public $data = [];
290
291    /**
292     * @var string
293     */
294    protected $table = '';
295
296    /**
297     * Used for backup
298     *
299     * @var array
300     */
301    public $oldData = [];
302
303    /**
304     * If this is set with an array before stdWrap, it's used instead of $this->data in the data-property in stdWrap
305     *
306     * @var string
307     */
308    public $alternativeData = '';
309
310    /**
311     * Used by the parseFunc function and is loaded with tag-parameters when parsing tags.
312     *
313     * @var array
314     */
315    public $parameters = [];
316
317    /**
318     * @var string
319     */
320    public $currentValKey = 'currentValue_kidjls9dksoje';
321
322    /**
323     * This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation.
324     * Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered.
325     *
326     * @var string
327     */
328    public $currentRecord = '';
329
330    /**
331     * Set in RecordsContentObject and ContentContentObject to the current number of records selected in a query.
332     *
333     * @var int
334     */
335    public $currentRecordTotal = 0;
336
337    /**
338     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
339     *
340     * @var int
341     */
342    public $currentRecordNumber = 0;
343
344    /**
345     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
346     *
347     * @var int
348     */
349    public $parentRecordNumber = 0;
350
351    /**
352     * 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.
353     *
354     * @var array
355     */
356    public $parentRecord = [];
357
358    /**
359     * This is used by checkPid, that checks if pages are accessible. The $checkPid_cache['page_uid'] is set TRUE or FALSE upon this check featuring a caching function for the next request.
360     *
361     * @var array
362     */
363    public $checkPid_cache = [];
364
365    /**
366     * @var string
367     */
368    public $checkPid_badDoktypeList = '255';
369
370    /**
371     * This will be set by typoLink() to the url of the most recent link created.
372     *
373     * @var string
374     */
375    public $lastTypoLinkUrl = '';
376
377    /**
378     * DO. link target.
379     *
380     * @var string
381     */
382    public $lastTypoLinkTarget = '';
383
384    /**
385     * @var array
386     */
387    public $lastTypoLinkLD = [];
388
389    /**
390     * array that registers rendered content elements (or any table) to make sure they are not rendered recursively!
391     *
392     * @var array
393     */
394    public $recordRegister = [];
395
396    /**
397     * Additionally registered content object types and class names
398     *
399     * @var array
400     */
401    protected $cObjHookObjectsRegistry = [];
402
403    /**
404     * @var array
405     */
406    public $cObjHookObjectsArr = [];
407
408    /**
409     * Containing hook objects for stdWrap
410     *
411     * @var array
412     */
413    protected $stdWrapHookObjects = [];
414
415    /**
416     * Containing hook objects for getImgResource
417     *
418     * @var array
419     */
420    protected $getImgResourceHookObjects;
421
422    /**
423     * @var File Current file objects (during iterations over files)
424     */
425    protected $currentFile;
426
427    /**
428     * Set to TRUE by doConvertToUserIntObject() if USER object wants to become USER_INT
429     */
430    public $doConvertToUserIntObject = false;
431
432    /**
433     * Indicates current object type. Can hold one of OBJECTTYPE_ constants or FALSE.
434     * The value is set and reset inside USER() function. Any time outside of
435     * USER() it is FALSE.
436     */
437    protected $userObjectType = false;
438
439    /**
440     * @var array
441     */
442    protected $stopRendering = [];
443
444    /**
445     * @var int
446     */
447    protected $stdWrapRecursionLevel = 0;
448
449    /**
450     * @var TypoScriptFrontendController
451     */
452    protected $typoScriptFrontendController;
453
454    /**
455     * Indicates that object type is USER.
456     *
457     * @see ContentObjectRender::$userObjectType
458     */
459    const OBJECTTYPE_USER_INT = 1;
460    /**
461     * Indicates that object type is USER.
462     *
463     * @see ContentObjectRender::$userObjectType
464     */
465    const OBJECTTYPE_USER = 2;
466
467    /**
468     * @param TypoScriptFrontendController $typoScriptFrontendController
469     */
470    public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null)
471    {
472        $this->typoScriptFrontendController = $typoScriptFrontendController;
473        $this->contentObjectClassMap = $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'];
474    }
475
476    /**
477     * Prevent several objects from being serialized.
478     * If currentFile is set, it is either a File or a FileReference object. As the object itself can't be serialized,
479     * we have store a hash and restore the object in __wakeup()
480     *
481     * @return array
482     */
483    public function __sleep()
484    {
485        $vars = get_object_vars($this);
486        unset($vars['typoScriptFrontendController'], $vars['logger']);
487        if ($this->currentFile instanceof FileReference) {
488            $this->currentFile = 'FileReference:' . $this->currentFile->getUid();
489        } elseif ($this->currentFile instanceof File) {
490            $this->currentFile = 'File:' . $this->currentFile->getIdentifier();
491        } else {
492            unset($vars['currentFile']);
493        }
494        return array_keys($vars);
495    }
496
497    /**
498     * Restore currentFile from hash.
499     * If currentFile references a File, the identifier equals file identifier.
500     * If it references a FileReference the identifier equals the uid of the reference.
501     */
502    public function __wakeup()
503    {
504        if (isset($GLOBALS['TSFE'])) {
505            $this->typoScriptFrontendController = $GLOBALS['TSFE'];
506        }
507        if ($this->currentFile !== null && is_string($this->currentFile)) {
508            list($objectType, $identifier) = explode(':', $this->currentFile, 2);
509            try {
510                if ($objectType === 'File') {
511                    $this->currentFile = ResourceFactory::getInstance()->retrieveFileOrFolderObject($identifier);
512                } elseif ($objectType === 'FileReference') {
513                    $this->currentFile = ResourceFactory::getInstance()->getFileReferenceObject($identifier);
514                }
515            } catch (ResourceDoesNotExistException $e) {
516                $this->currentFile = null;
517            }
518        }
519        $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
520    }
521
522    /**
523     * Allow injecting content object class map.
524     *
525     * This method is private API, please use configuration
526     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
527     *
528     * @internal
529     * @param array $contentObjectClassMap
530     */
531    public function setContentObjectClassMap(array $contentObjectClassMap)
532    {
533        $this->contentObjectClassMap = $contentObjectClassMap;
534    }
535
536    /**
537     * Register a single content object name to class name
538     *
539     * This method is private API, please use configuration
540     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
541     *
542     * @param string $className
543     * @param string $contentObjectName
544     * @internal
545     */
546    public function registerContentObjectClass($className, $contentObjectName)
547    {
548        $this->contentObjectClassMap[$contentObjectName] = $className;
549    }
550
551    /**
552     * Class constructor.
553     * Well, it has to be called manually since it is not a real constructor function.
554     * 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.
555     *
556     * @param array $data The record data that is rendered.
557     * @param string $table The table that the data record is from.
558     */
559    public function start($data, $table = '')
560    {
561        $this->data = $data;
562        $this->table = $table;
563        $this->currentRecord = $table !== ''
564            ? $table . ':' . ($this->data['uid'] ?? '')
565            : '';
566        $this->parameters = [];
567        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClass'] ?? [] as $classArr) {
568            $this->cObjHookObjectsRegistry[$classArr[0]] = $classArr[1];
569        }
570        $this->stdWrapHookObjects = [];
571        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] ?? [] as $className) {
572            $hookObject = GeneralUtility::makeInstance($className);
573            if (!$hookObject instanceof ContentObjectStdWrapHookInterface) {
574                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectStdWrapHookInterface::class, 1195043965);
575            }
576            $this->stdWrapHookObjects[] = $hookObject;
577        }
578        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] ?? [] as $className) {
579            $postInitializationProcessor = GeneralUtility::makeInstance($className);
580            if (!$postInitializationProcessor instanceof ContentObjectPostInitHookInterface) {
581                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectPostInitHookInterface::class, 1274563549);
582            }
583            $postInitializationProcessor->postProcessContentObjectInitialization($this);
584        }
585    }
586
587    /**
588     * Returns the current table
589     *
590     * @return string
591     */
592    public function getCurrentTable()
593    {
594        return $this->table;
595    }
596
597    /**
598     * Gets the 'getImgResource' hook objects.
599     * The first call initializes the accordant objects.
600     *
601     * @return array The 'getImgResource' hook objects (if any)
602     */
603    protected function getGetImgResourceHookObjects()
604    {
605        if (!isset($this->getImgResourceHookObjects)) {
606            $this->getImgResourceHookObjects = [];
607            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] ?? [] as $className) {
608                $hookObject = GeneralUtility::makeInstance($className);
609                if (!$hookObject instanceof ContentObjectGetImageResourceHookInterface) {
610                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetImageResourceHookInterface::class, 1218636383);
611                }
612                $this->getImgResourceHookObjects[] = $hookObject;
613            }
614        }
615        return $this->getImgResourceHookObjects;
616    }
617
618    /**
619     * Sets the internal variable parentRecord with information about current record.
620     * 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.
621     *
622     * @param array $data The record array
623     * @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.
624     * @internal
625     */
626    public function setParent($data, $currentRecord)
627    {
628        $this->parentRecord = [
629            'data' => $data,
630            'currentRecord' => $currentRecord
631        ];
632    }
633
634    /***********************************************
635     *
636     * CONTENT_OBJ:
637     *
638     ***********************************************/
639    /**
640     * Returns the "current" value.
641     * 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.
642     * It's like "load accumulator" in the good old C64 days... basically a "register" you can use as you like.
643     * 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.
644     *
645     * @return mixed The "current" value
646     */
647    public function getCurrentVal()
648    {
649        return $this->data[$this->currentValKey];
650    }
651
652    /**
653     * Sets the "current" value.
654     *
655     * @param mixed $value The variable that you want to set as "current
656     * @see getCurrentVal()
657     */
658    public function setCurrentVal($value)
659    {
660        $this->data[$this->currentValKey] = $value;
661    }
662
663    /**
664     * Rendering of a "numerical array" of cObjects from TypoScript
665     * Will call ->cObjGetSingle() for each cObject found and accumulate the output.
666     *
667     * @param array $setup array with cObjects as values.
668     * @param string $addKey A prefix for the debugging information
669     * @return string Rendered output from the cObjects in the array.
670     * @see cObjGetSingle()
671     */
672    public function cObjGet($setup, $addKey = '')
673    {
674        if (!is_array($setup)) {
675            return '';
676        }
677        $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($setup);
678        $content = '';
679        foreach ($sKeyArray as $theKey) {
680            $theValue = $setup[$theKey];
681            if ((int)$theKey && strpos($theKey, '.') === false) {
682                $conf = $setup[$theKey . '.'];
683                $content .= $this->cObjGetSingle($theValue, $conf, $addKey . $theKey);
684            }
685        }
686        return $content;
687    }
688
689    /**
690     * Renders a content object
691     *
692     * @param string $name The content object name, eg. "TEXT" or "USER" or "IMAGE
693     * @param array $conf The array with TypoScript properties for the content object
694     * @param string $TSkey A string label used for the internal debugging tracking.
695     * @return string cObject output
696     * @throws \UnexpectedValueException
697     */
698    public function cObjGetSingle($name, $conf, $TSkey = '__')
699    {
700        $content = '';
701        // Checking that the function is not called eternally. This is done by interrupting at a depth of 100
702        $this->getTypoScriptFrontendController()->cObjectDepthCounter--;
703        if ($this->getTypoScriptFrontendController()->cObjectDepthCounter > 0) {
704            $timeTracker = $this->getTimeTracker();
705            $name = trim($name);
706            if ($timeTracker->LR) {
707                $timeTracker->push($TSkey, $name);
708            }
709            // Checking if the COBJ is a reference to another object. (eg. name of 'some.object =< styles.something')
710            if (isset($name[0]) && $name[0] === '<') {
711                $key = trim(substr($name, 1));
712                $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
713                // $name and $conf is loaded with the referenced values.
714                $confOverride = is_array($conf) ? $conf : [];
715                list($name, $conf) = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
716                $conf = array_replace_recursive(is_array($conf) ? $conf : [], $confOverride);
717                // Getting the cObject
718                $timeTracker->incStackPointer();
719                $content .= $this->cObjGetSingle($name, $conf, $key);
720                $timeTracker->decStackPointer();
721            } else {
722                $hooked = false;
723                // Application defined cObjects
724                if (!empty($this->cObjHookObjectsRegistry[$name])) {
725                    if (empty($this->cObjHookObjectsArr[$name])) {
726                        $this->cObjHookObjectsArr[$name] = GeneralUtility::makeInstance($this->cObjHookObjectsRegistry[$name]);
727                    }
728                    $hookObj = $this->cObjHookObjectsArr[$name];
729                    if (method_exists($hookObj, 'cObjGetSingleExt')) {
730                        $content .= $hookObj->cObjGetSingleExt($name, $conf, $TSkey, $this);
731                        $hooked = true;
732                    }
733                }
734                if (!$hooked) {
735                    $contentObject = $this->getContentObject($name);
736                    if ($contentObject) {
737                        $content .= $this->render($contentObject, $conf);
738                    } else {
739                        // Call hook functions for extra processing
740                        if ($name) {
741                            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClassDefault'] ?? [] as $className) {
742                                $hookObject = GeneralUtility::makeInstance($className);
743                                if (!$hookObject instanceof ContentObjectGetSingleHookInterface) {
744                                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetSingleHookInterface::class, 1195043731);
745                                }
746                                /** @var ContentObjectGetSingleHookInterface $hookObject */
747                                $content .= $hookObject->getSingleContentObject($name, (array)$conf, $TSkey, $this);
748                            }
749                        } else {
750                            // Log error in AdminPanel
751                            $warning = sprintf('Content Object "%s" does not exist', $name);
752                            $timeTracker->setTSlogMessage($warning, 2);
753                        }
754                    }
755                }
756            }
757            if ($timeTracker->LR) {
758                $timeTracker->pull($content);
759            }
760        }
761        // Increasing on exit...
762        $this->getTypoScriptFrontendController()->cObjectDepthCounter++;
763        return $content;
764    }
765
766    /**
767     * Returns a new content object of type $name.
768     * This content object needs to be registered as content object
769     * in $this->contentObjectClassMap
770     *
771     * @param string $name
772     * @return AbstractContentObject|null
773     * @throws ContentRenderingException
774     */
775    public function getContentObject($name)
776    {
777        if (!isset($this->contentObjectClassMap[$name])) {
778            return null;
779        }
780        $fullyQualifiedClassName = $this->contentObjectClassMap[$name];
781        $contentObject = GeneralUtility::makeInstance($fullyQualifiedClassName, $this);
782        if (!($contentObject instanceof AbstractContentObject)) {
783            throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295);
784        }
785        return $contentObject;
786    }
787
788    /********************************************
789     *
790     * Functions rendering content objects (cObjects)
791     *
792     ********************************************/
793
794    /**
795     * Renders a content object by taking exception and cache handling
796     * into consideration
797     *
798     * @param AbstractContentObject $contentObject Content object instance
799     * @param array $configuration Array of TypoScript properties
800     *
801     * @throws ContentRenderingException
802     * @throws \Exception
803     * @return string
804     */
805    public function render(AbstractContentObject $contentObject, $configuration = [])
806    {
807        $content = '';
808
809        // Evaluate possible cache and return
810        $cacheConfiguration = $configuration['cache.'] ?? null;
811        if ($cacheConfiguration !== null) {
812            unset($configuration['cache.']);
813            $cache = $this->getFromCache($cacheConfiguration);
814            if ($cache !== false) {
815                return $cache;
816            }
817        }
818
819        // Render content
820        try {
821            $content .= $contentObject->render($configuration);
822        } catch (ContentRenderingException $exception) {
823            // Content rendering Exceptions indicate a critical problem which should not be
824            // caught e.g. when something went wrong with Exception handling itself
825            throw $exception;
826        } catch (\Exception $exception) {
827            $exceptionHandler = $this->createExceptionHandler($configuration);
828            if ($exceptionHandler === null) {
829                throw $exception;
830            }
831            $content = $exceptionHandler->handle($exception, $contentObject, $configuration);
832        }
833
834        // Store cache
835        if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) {
836            $key = $this->calculateCacheKey($cacheConfiguration);
837            if (!empty($key)) {
838                /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
839                $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
840                $tags = $this->calculateCacheTags($cacheConfiguration);
841                $lifetime = $this->calculateCacheLifetime($cacheConfiguration);
842                $cacheFrontend->set($key, $content, $tags, $lifetime);
843            }
844        }
845
846        return $content;
847    }
848
849    /**
850     * Creates the content object exception handler from local content object configuration
851     * or, from global configuration if not explicitly disabled in local configuration
852     *
853     * @param array $configuration
854     * @return ExceptionHandlerInterface|null
855     * @throws ContentRenderingException
856     */
857    protected function createExceptionHandler($configuration = [])
858    {
859        $exceptionHandler = null;
860        $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration);
861        if (!empty($exceptionHandlerClassName)) {
862            $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName, $this->mergeExceptionHandlerConfiguration($configuration));
863            if (!$exceptionHandler instanceof ExceptionHandlerInterface) {
864                throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369);
865            }
866        }
867
868        return $exceptionHandler;
869    }
870
871    /**
872     * Determine exception handler class name from global and content object configuration
873     *
874     * @param array $configuration
875     * @return string|null
876     */
877    protected function determineExceptionHandlerClassName($configuration)
878    {
879        $exceptionHandlerClassName = null;
880        $tsfe = $this->getTypoScriptFrontendController();
881        if (!isset($tsfe->config['config']['contentObjectExceptionHandler'])) {
882            if (GeneralUtility::getApplicationContext()->isProduction()) {
883                $exceptionHandlerClassName = '1';
884            }
885        } else {
886            $exceptionHandlerClassName = $tsfe->config['config']['contentObjectExceptionHandler'];
887        }
888
889        if (isset($configuration['exceptionHandler'])) {
890            $exceptionHandlerClassName = $configuration['exceptionHandler'];
891        }
892
893        if ($exceptionHandlerClassName === '1') {
894            $exceptionHandlerClassName = ProductionExceptionHandler::class;
895        }
896
897        return $exceptionHandlerClassName;
898    }
899
900    /**
901     * Merges global exception handler configuration with the one from the content object
902     * and returns the merged exception handler configuration
903     *
904     * @param array $configuration
905     * @return array
906     */
907    protected function mergeExceptionHandlerConfiguration($configuration)
908    {
909        $exceptionHandlerConfiguration = [];
910        $tsfe = $this->getTypoScriptFrontendController();
911        if (!empty($tsfe->config['config']['contentObjectExceptionHandler.'])) {
912            $exceptionHandlerConfiguration = $tsfe->config['config']['contentObjectExceptionHandler.'];
913        }
914        if (!empty($configuration['exceptionHandler.'])) {
915            $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']);
916        }
917
918        return $exceptionHandlerConfiguration;
919    }
920
921    /**
922     * Retrieves a type of object called as USER or USER_INT. Object can detect their
923     * type by using this call. It returns OBJECTTYPE_USER_INT or OBJECTTYPE_USER depending on the
924     * current object execution. In all other cases it will return FALSE to indicate
925     * a call out of context.
926     *
927     * @return mixed One of OBJECTTYPE_ class constants or FALSE
928     */
929    public function getUserObjectType()
930    {
931        return $this->userObjectType;
932    }
933
934    /**
935     * Sets the user object type
936     *
937     * @param mixed $userObjectType
938     */
939    public function setUserObjectType($userObjectType)
940    {
941        $this->userObjectType = $userObjectType;
942    }
943
944    /**
945     * Requests the current USER object to be converted to USER_INT.
946     */
947    public function convertToUserIntObject()
948    {
949        if ($this->userObjectType !== self::OBJECTTYPE_USER) {
950            $this->getTimeTracker()->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', 2);
951        } else {
952            $this->doConvertToUserIntObject = true;
953        }
954    }
955
956    /************************************
957     *
958     * Various helper functions for content objects:
959     *
960     ************************************/
961    /**
962     * Converts a given config in Flexform to a conf-array
963     *
964     * @param string|array $flexData Flexform data
965     * @param array $conf Array to write the data into, by reference
966     * @param bool $recursive Is set if called recursive. Don't call function with this parameter, it's used inside the function only
967     */
968    public function readFlexformIntoConf($flexData, &$conf, $recursive = false)
969    {
970        if ($recursive === false && is_string($flexData)) {
971            $flexData = GeneralUtility::xml2array($flexData, 'T3');
972        }
973        if (is_array($flexData) && isset($flexData['data']['sDEF']['lDEF'])) {
974            $flexData = $flexData['data']['sDEF']['lDEF'];
975        }
976        if (!is_array($flexData)) {
977            return;
978        }
979        foreach ($flexData as $key => $value) {
980            if (!is_array($value)) {
981                continue;
982            }
983            if (isset($value['el'])) {
984                if (is_array($value['el']) && !empty($value['el'])) {
985                    foreach ($value['el'] as $ekey => $element) {
986                        if (isset($element['vDEF'])) {
987                            $conf[$ekey] = $element['vDEF'];
988                        } else {
989                            if (is_array($element)) {
990                                $this->readFlexformIntoConf($element, $conf[$key][key($element)][$ekey], true);
991                            } else {
992                                $this->readFlexformIntoConf($element, $conf[$key][$ekey], true);
993                            }
994                        }
995                    }
996                } else {
997                    $this->readFlexformIntoConf($value['el'], $conf[$key], true);
998                }
999            }
1000            if (isset($value['vDEF'])) {
1001                $conf[$key] = $value['vDEF'];
1002            }
1003        }
1004    }
1005
1006    /**
1007     * Returns all parents of the given PID (Page UID) list
1008     *
1009     * @param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap
1010     * @param array $pidConf stdWrap array for the list
1011     * @return string A list of PIDs
1012     * @internal
1013     */
1014    public function getSlidePids($pidList, $pidConf)
1015    {
1016        $pidList = isset($pidConf) ? trim($this->stdWrap($pidList, $pidConf)) : trim($pidList);
1017        if ($pidList === '') {
1018            $pidList = 'this';
1019        }
1020        $tsfe = $this->getTypoScriptFrontendController();
1021        $listArr = null;
1022        if (trim($pidList)) {
1023            $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $pidList));
1024            $listArr = $this->checkPidArray($listArr);
1025        }
1026        $pidList = [];
1027        if (is_array($listArr) && !empty($listArr)) {
1028            foreach ($listArr as $uid) {
1029                $page = $tsfe->sys_page->getPage($uid);
1030                if (!$page['is_siteroot']) {
1031                    $pidList[] = $page['pid'];
1032                }
1033            }
1034        }
1035        return implode(',', $pidList);
1036    }
1037
1038    /**
1039     * Returns a <img> tag with the image file defined by $file and processed according to the properties in the TypoScript array.
1040     * Mostly this function is a sub-function to the IMAGE function which renders the IMAGE cObject in TypoScript.
1041     * This function is called by "$this->cImage($conf['file'], $conf);" from IMAGE().
1042     *
1043     * @param string $file File TypoScript resource
1044     * @param array $conf TypoScript configuration properties
1045     * @return string <img> tag, (possibly wrapped in links and other HTML) if any image found.
1046     * @internal
1047     * @see IMAGE()
1048     */
1049    public function cImage($file, $conf)
1050    {
1051        $tsfe = $this->getTypoScriptFrontendController();
1052        $info = $this->getImgResource($file, $conf['file.']);
1053        $tsfe->lastImageInfo = $info;
1054        if (!is_array($info)) {
1055            return '';
1056        }
1057        if (is_file(Environment::getPublicPath() . '/' . $info['3'])) {
1058            $source = $tsfe->absRefPrefix . str_replace('%2F', '/', rawurlencode($info['3']));
1059        } else {
1060            $source = $info[3];
1061        }
1062
1063        $layoutKey = $this->stdWrap($conf['layoutKey'], $conf['layoutKey.']);
1064        $imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf);
1065        $sourceCollection = $this->getImageSourceCollection($layoutKey, $conf, $file);
1066
1067        // This array is used to collect the image-refs on the page...
1068        $tsfe->imagesOnPage[] = $source;
1069        $altParam = $this->getAltParam($conf);
1070        $params = $this->stdWrapValue('params', $conf);
1071        if ($params !== '' && $params[0] !== ' ') {
1072            $params = ' ' . $params;
1073        }
1074
1075        $imageTagValues = [
1076            'width' =>  (int)$info[0],
1077            'height' => (int)$info[1],
1078            'src' => htmlspecialchars($source),
1079            'params' => $params,
1080            'altParams' => $altParam,
1081            'border' =>  $this->getBorderAttr(' border="' . (int)$conf['border'] . '"'),
1082            'sourceCollection' => $sourceCollection,
1083            'selfClosingTagSlash' => !empty($tsfe->xhtmlDoctype) ? ' /' : '',
1084        ];
1085
1086        $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1087        $theValue = $markerTemplateEngine->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
1088
1089        $linkWrap = isset($conf['linkWrap.']) ? $this->stdWrap($conf['linkWrap'], $conf['linkWrap.']) : $conf['linkWrap'];
1090        if ($linkWrap) {
1091            $theValue = $this->linkWrap($theValue, $linkWrap);
1092        } elseif ($conf['imageLinkWrap']) {
1093            $originalFile = !empty($info['originalFile']) ? $info['originalFile'] : $info['origFile'];
1094            $theValue = $this->imageLinkWrap($theValue, $originalFile, $conf['imageLinkWrap.']);
1095        }
1096        $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
1097        if ((string)$wrap !== '') {
1098            $theValue = $this->wrap($theValue, $conf['wrap']);
1099        }
1100        return $theValue;
1101    }
1102
1103    /**
1104     * Returns the 'border' attribute for an <img> tag only if the doctype is not xhtml_strict, xhtml_11 or html5
1105     * or if the config parameter 'disableImgBorderAttr' is not set.
1106     *
1107     * @param string $borderAttr The border attribute
1108     * @return string The border attribute
1109     */
1110    public function getBorderAttr($borderAttr)
1111    {
1112        $tsfe = $this->getTypoScriptFrontendController();
1113        $docType = $tsfe->xhtmlDoctype;
1114        if (
1115            $docType !== 'xhtml_strict' && $docType !== 'xhtml_11'
1116            && $tsfe->config['config']['doctype'] !== 'html5'
1117            && !$tsfe->config['config']['disableImgBorderAttr']
1118        ) {
1119            return $borderAttr;
1120        }
1121        return '';
1122    }
1123
1124    /**
1125     * Returns the html-template for rendering the image-Tag if no template is defined via typoscript the
1126     * default <img> tag template is returned
1127     *
1128     * @param string $layoutKey rendering key
1129     * @param array $conf TypoScript configuration properties
1130     * @return string
1131     */
1132    public function getImageTagTemplate($layoutKey, $conf)
1133    {
1134        if ($layoutKey && isset($conf['layout.']) && isset($conf['layout.'][$layoutKey . '.'])) {
1135            $imageTagLayout = $this->stdWrap(
1136                $conf['layout.'][$layoutKey . '.']['element'] ?? '',
1137                $conf['layout.'][$layoutKey . '.']['element.'] ?? []
1138            );
1139        } else {
1140            $imageTagLayout = '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###BORDER######SELFCLOSINGTAGSLASH###>';
1141        }
1142        return $imageTagLayout;
1143    }
1144
1145    /**
1146     * Render alternate sources for the image tag. If no source collection is given an empty string is returned.
1147     *
1148     * @param string $layoutKey rendering key
1149     * @param array $conf TypoScript configuration properties
1150     * @param string $file
1151     * @throws \UnexpectedValueException
1152     * @return string
1153     */
1154    public function getImageSourceCollection($layoutKey, $conf, $file)
1155    {
1156        $sourceCollection = '';
1157        if ($layoutKey
1158            && isset($conf['sourceCollection.']) && $conf['sourceCollection.']
1159            && (
1160                isset($conf['layout.'][$layoutKey . '.']['source']) && $conf['layout.'][$layoutKey . '.']['source']
1161                || isset($conf['layout.'][$layoutKey . '.']['source.']) && $conf['layout.'][$layoutKey . '.']['source.']
1162            )
1163        ) {
1164
1165            // find active sourceCollection
1166            $activeSourceCollections = [];
1167            foreach ($conf['sourceCollection.'] as $sourceCollectionKey => $sourceCollectionConfiguration) {
1168                if (substr($sourceCollectionKey, -1) === '.') {
1169                    if (empty($sourceCollectionConfiguration['if.']) || $this->checkIf($sourceCollectionConfiguration['if.'])) {
1170                        $activeSourceCollections[] = $sourceCollectionConfiguration;
1171                    }
1172                }
1173            }
1174
1175            // apply option split to configurations
1176            $tsfe = $this->getTypoScriptFrontendController();
1177            $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
1178            $srcLayoutOptionSplitted = $typoScriptService->explodeConfigurationForOptionSplit((array)$conf['layout.'][$layoutKey . '.'], count($activeSourceCollections));
1179
1180            // render sources
1181            foreach ($activeSourceCollections as $key => $sourceConfiguration) {
1182                $sourceLayout = $this->stdWrap(
1183                    $srcLayoutOptionSplitted[$key]['source'] ?? '',
1184                    $srcLayoutOptionSplitted[$key]['source.'] ?? []
1185                );
1186
1187                $sourceRenderConfiguration = [
1188                    'file' => $file,
1189                    'file.' => $conf['file.'] ?? null
1190                ];
1191
1192                if (isset($sourceConfiguration['quality']) || isset($sourceConfiguration['quality.'])) {
1193                    $imageQuality = $sourceConfiguration['quality'] ?? '';
1194                    if (isset($sourceConfiguration['quality.'])) {
1195                        $imageQuality = $this->stdWrap($sourceConfiguration['quality'], $sourceConfiguration['quality.']);
1196                    }
1197                    if ($imageQuality) {
1198                        $sourceRenderConfiguration['file.']['params'] = '-quality ' . (int)$imageQuality;
1199                    }
1200                }
1201
1202                if (isset($sourceConfiguration['pixelDensity'])) {
1203                    $pixelDensity = (int)$this->stdWrap(
1204                        $sourceConfiguration['pixelDensity'] ?? '',
1205                        $sourceConfiguration['pixelDensity.'] ?? []
1206                    );
1207                } else {
1208                    $pixelDensity = 1;
1209                }
1210                $dimensionKeys = ['width', 'height', 'maxW', 'minW', 'maxH', 'minH', 'maxWidth', 'maxHeight', 'XY'];
1211                foreach ($dimensionKeys as $dimensionKey) {
1212                    $dimension = $this->stdWrap(
1213                        $sourceConfiguration[$dimensionKey] ?? '',
1214                        $sourceConfiguration[$dimensionKey . '.'] ?? []
1215                    );
1216                    if (!$dimension) {
1217                        $dimension = $this->stdWrap(
1218                            $conf['file.'][$dimensionKey] ?? '',
1219                            $conf['file.'][$dimensionKey . '.'] ?? []
1220                        );
1221                    }
1222                    if ($dimension) {
1223                        if (strstr($dimension, 'c') !== false && ($dimensionKey === 'width' || $dimensionKey === 'height')) {
1224                            $dimensionParts = explode('c', $dimension, 2);
1225                            $dimension = ((int)$dimensionParts[0] * $pixelDensity) . 'c';
1226                            if ($dimensionParts[1]) {
1227                                $dimension .= $dimensionParts[1];
1228                            }
1229                        } elseif ($dimensionKey === 'XY') {
1230                            $dimensionParts = GeneralUtility::intExplode(',', $dimension, false, 2);
1231                            $dimension = $dimensionParts[0] * $pixelDensity;
1232                            if ($dimensionParts[1]) {
1233                                $dimension .= ',' . $dimensionParts[1] * $pixelDensity;
1234                            }
1235                        } else {
1236                            $dimension = (int)$dimension * $pixelDensity;
1237                        }
1238                        $sourceRenderConfiguration['file.'][$dimensionKey] = $dimension;
1239                        // Remove the stdWrap properties for dimension as they have been processed already above.
1240                        unset($sourceRenderConfiguration['file.'][$dimensionKey . '.']);
1241                    }
1242                }
1243                $sourceInfo = $this->getImgResource($sourceRenderConfiguration['file'], $sourceRenderConfiguration['file.']);
1244                if ($sourceInfo) {
1245                    $sourceConfiguration['width'] = $sourceInfo[0];
1246                    $sourceConfiguration['height'] = $sourceInfo[1];
1247                    $urlPrefix = '';
1248                    if (parse_url($sourceInfo[3], PHP_URL_HOST) === null) {
1249                        $urlPrefix = $tsfe->absRefPrefix;
1250                    }
1251                    $sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $sourceInfo[3]);
1252                    $sourceConfiguration['selfClosingTagSlash'] = !empty($tsfe->xhtmlDoctype) ? ' /' : '';
1253
1254                    $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1255                    $oneSourceCollection = $markerTemplateEngine->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
1256
1257                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImageSourceCollection'] ?? [] as $className) {
1258                        $hookObject = GeneralUtility::makeInstance($className);
1259                        if (!$hookObject instanceof ContentObjectOneSourceCollectionHookInterface) {
1260                            throw new \UnexpectedValueException(
1261                                '$hookObject must implement interface ' . ContentObjectOneSourceCollectionHookInterface::class,
1262                                1380007853
1263                            );
1264                        }
1265                        $oneSourceCollection = $hookObject->getOneSourceCollection((array)$sourceRenderConfiguration, (array)$sourceConfiguration, $oneSourceCollection, $this);
1266                    }
1267
1268                    $sourceCollection .= $oneSourceCollection;
1269                }
1270            }
1271        }
1272        return $sourceCollection;
1273    }
1274
1275    /**
1276     * Wraps the input string in link-tags that opens the image in a new window.
1277     *
1278     * @param string $string String to wrap, probably an <img> tag
1279     * @param string|File|FileReference $imageFile The original image file
1280     * @param array $conf TypoScript properties for the "imageLinkWrap" function
1281     * @return string The input string, $string, wrapped as configured.
1282     * @see cImage()
1283     */
1284    public function imageLinkWrap($string, $imageFile, $conf)
1285    {
1286        $string = (string)$string;
1287        $enable = isset($conf['enable.']) ? $this->stdWrap($conf['enable'], $conf['enable.']) : $conf['enable'];
1288        if (!$enable) {
1289            return $string;
1290        }
1291        $content = (string)$this->typoLink($string, $conf['typolink.']);
1292        if (isset($conf['file.'])) {
1293            $imageFile = $this->stdWrap($imageFile, $conf['file.']);
1294        }
1295
1296        if ($imageFile instanceof File) {
1297            $file = $imageFile;
1298        } elseif ($imageFile instanceof FileReference) {
1299            $file = $imageFile->getOriginalFile();
1300        } else {
1301            if (MathUtility::canBeInterpretedAsInteger($imageFile)) {
1302                $file = ResourceFactory::getInstance()->getFileObject((int)$imageFile);
1303            } else {
1304                $file = ResourceFactory::getInstance()->getFileObjectFromCombinedIdentifier($imageFile);
1305            }
1306        }
1307
1308        // Create imageFileLink if not created with typolink
1309        if ($content === $string && $file !== null) {
1310            $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop'];
1311            $parameters = [];
1312            $sample = isset($conf['sample.']) ? $this->stdWrap($conf['sample'], $conf['sample.']) : $conf['sample'];
1313            if ($sample) {
1314                $parameters['sample'] = 1;
1315            }
1316            foreach ($parameterNames as $parameterName) {
1317                if (isset($conf[$parameterName . '.'])) {
1318                    $conf[$parameterName] = $this->stdWrap($conf[$parameterName], $conf[$parameterName . '.']);
1319                }
1320                if (isset($conf[$parameterName]) && $conf[$parameterName]) {
1321                    $parameters[$parameterName] = $conf[$parameterName];
1322                }
1323            }
1324            $parametersEncoded = base64_encode(json_encode($parameters));
1325            $hmac = GeneralUtility::hmac(implode('|', [$file->getUid(), $parametersEncoded]));
1326            $params = '&md5=' . $hmac;
1327            foreach (str_split($parametersEncoded, 64) as $index => $chunk) {
1328                $params .= '&parameters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk);
1329            }
1330            $url = $this->getTypoScriptFrontendController()->absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params;
1331            $directImageLink = isset($conf['directImageLink.']) ? $this->stdWrap($conf['directImageLink'], $conf['directImageLink.']) : $conf['directImageLink'];
1332            if ($directImageLink) {
1333                $imgResourceConf = [
1334                    'file' => $imageFile,
1335                    'file.' => $conf
1336                ];
1337                $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf);
1338                if (!$url) {
1339                    // If no imagemagick / gm is available
1340                    $url = $imageFile;
1341                }
1342            }
1343            // Create TARGET-attribute only if the right doctype is used
1344            $target = '';
1345            $xhtmlDocType = $this->getTypoScriptFrontendController()->xhtmlDoctype;
1346            if ($xhtmlDocType !== 'xhtml_strict' && $xhtmlDocType !== 'xhtml_11') {
1347                $target = isset($conf['target.'])
1348                    ? (string)$this->stdWrap($conf['target'], $conf['target.'])
1349                    : (string)$conf['target'];
1350                if ($target === '') {
1351                    $target = 'thePicture';
1352                }
1353            }
1354            $a1 = '';
1355            $a2 = '';
1356            $conf['JSwindow'] = isset($conf['JSwindow.']) ? $this->stdWrap($conf['JSwindow'], $conf['JSwindow.']) : $conf['JSwindow'];
1357            if ($conf['JSwindow']) {
1358                if ($conf['JSwindow.']['altUrl'] || $conf['JSwindow.']['altUrl.']) {
1359                    $altUrl = isset($conf['JSwindow.']['altUrl.']) ? $this->stdWrap($conf['JSwindow.']['altUrl'], $conf['JSwindow.']['altUrl.']) : $conf['JSwindow.']['altUrl'];
1360                    if ($altUrl) {
1361                        $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode($imageFile) . $params);
1362                    }
1363                }
1364
1365                $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf);
1366                $JSwindowExpand = isset($conf['JSwindow.']['expand.']) ? $this->stdWrap($conf['JSwindow.']['expand'], $conf['JSwindow.']['expand.']) : $conf['JSwindow.']['expand'];
1367                $offset = GeneralUtility::intExplode(',', $JSwindowExpand . ',');
1368                $newWindow = isset($conf['JSwindow.']['newWindow.']) ? $this->stdWrap($conf['JSwindow.']['newWindow'], $conf['JSwindow.']['newWindow.']) : $conf['JSwindow.']['newWindow'];
1369                $onClick = 'openPic('
1370                    . GeneralUtility::quoteJSvalue($this->getTypoScriptFrontendController()->baseUrlWrap($url)) . ','
1371                    . '\'' . ($newWindow ? md5($url) : 'thePicture') . '\','
1372                    . GeneralUtility::quoteJSvalue('width=' . ($processedFile->getProperty('width') + $offset[0])
1373                        . ',height=' . ($processedFile->getProperty('height') + $offset[1]) . ',status=0,menubar=0')
1374                    . '); return false;';
1375                $a1 = '<a href="' . htmlspecialchars($url) . '"'
1376                    . ' onclick="' . htmlspecialchars($onClick) . '"'
1377                    . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
1378                    . $this->getTypoScriptFrontendController()->ATagParams . '>';
1379                $a2 = '</a>';
1380                $this->getTypoScriptFrontendController()->setJS('openPic');
1381            } else {
1382                $conf['linkParams.']['directImageLink'] = (bool)$conf['directImageLink'];
1383                $conf['linkParams.']['parameter'] = $url;
1384                $string = $this->typoLink($string, $conf['linkParams.']);
1385            }
1386            if (isset($conf['stdWrap.'])) {
1387                $string = $this->stdWrap($string, $conf['stdWrap.']);
1388            }
1389            $content = $a1 . $string . $a2;
1390        }
1391        return $content;
1392    }
1393
1394    /**
1395     * Sets the SYS_LASTCHANGED timestamp if input timestamp is larger than current value.
1396     * The SYS_LASTCHANGED timestamp can be used by various caching/indexing applications to determine if the page has new content.
1397     * Therefore you should call this function with the last-changed timestamp of any element you display.
1398     *
1399     * @param int $tstamp Unix timestamp (number of seconds since 1970)
1400     * @see TypoScriptFrontendController::setSysLastChanged()
1401     */
1402    public function lastChanged($tstamp)
1403    {
1404        $tstamp = (int)$tstamp;
1405        $tsfe = $this->getTypoScriptFrontendController();
1406        if ($tstamp > (int)$tsfe->register['SYS_LASTCHANGED']) {
1407            $tsfe->register['SYS_LASTCHANGED'] = $tstamp;
1408        }
1409    }
1410
1411    /**
1412     * Wraps the input string by the $wrap value and implements the "linkWrap" data type as well.
1413     * The "linkWrap" data type means that this function will find any integer encapsulated in {} (curly braces) in the first wrap part and substitute it with the corresponding page uid from the rootline where the found integer is pointing to the key in the rootline. See link below.
1414     *
1415     * @param string $content Input string
1416     * @param string $wrap A string where the first two parts separated by "|" (vertical line) will be wrapped around the input string
1417     * @return string Wrapped output string
1418     * @see wrap(), cImage(), FILE()
1419     */
1420    public function linkWrap($content, $wrap)
1421    {
1422        $wrapArr = explode('|', $wrap);
1423        if (preg_match('/\\{([0-9]*)\\}/', $wrapArr[0], $reg)) {
1424            $uid = $this->getTypoScriptFrontendController()->tmpl->rootLine[$reg[1]]['uid'] ?? null;
1425            if ($uid) {
1426                $wrapArr[0] = str_replace($reg[0], $uid, $wrapArr[0]);
1427            }
1428        }
1429        return trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
1430    }
1431
1432    /**
1433     * An abstraction method which creates an alt or title parameter for an HTML img, applet, area or input element and the FILE content element.
1434     * From the $conf array it implements the properties "altText", "titleText" and "longdescURL"
1435     *
1436     * @param array $conf TypoScript configuration properties
1437     * @param bool $longDesc If set, the longdesc attribute will be generated - must only be used for img elements!
1438     * @return string Parameter string containing alt and title parameters (if any)
1439     * @see IMGTEXT(), FILE(), FORM(), cImage(), filelink()
1440     */
1441    public function getAltParam($conf, $longDesc = true)
1442    {
1443        $altText = isset($conf['altText.']) ? trim($this->stdWrap($conf['altText'], $conf['altText.'])) : trim($conf['altText']);
1444        $titleText = isset($conf['titleText.']) ? trim($this->stdWrap($conf['titleText'], $conf['titleText.'])) : trim($conf['titleText']);
1445        if (isset($conf['longdescURL.']) && $this->getTypoScriptFrontendController()->config['config']['doctype'] !== 'html5') {
1446            $longDescUrl = $this->typoLink_URL($conf['longdescURL.']);
1447        } else {
1448            $longDescUrl = trim($conf['longdescURL']);
1449        }
1450        $longDescUrl = strip_tags($longDescUrl);
1451
1452        // "alt":
1453        $altParam = ' alt="' . htmlspecialchars($altText) . '"';
1454        // "title":
1455        $emptyTitleHandling = isset($conf['emptyTitleHandling.']) ? $this->stdWrap($conf['emptyTitleHandling'], $conf['emptyTitleHandling.']) : $conf['emptyTitleHandling'];
1456        // Choices: 'keepEmpty' | 'useAlt' | 'removeAttr'
1457        if ($titleText || $emptyTitleHandling === 'keepEmpty') {
1458            $altParam .= ' title="' . htmlspecialchars($titleText) . '"';
1459        } elseif (!$titleText && $emptyTitleHandling === 'useAlt') {
1460            $altParam .= ' title="' . htmlspecialchars($altText) . '"';
1461        }
1462        // "longDesc" URL
1463        if ($longDesc && !empty($longDescUrl)) {
1464            $altParam .= ' longdesc="' . htmlspecialchars($longDescUrl) . '"';
1465        }
1466        return $altParam;
1467    }
1468
1469    /**
1470     * An abstraction method to add parameters to an A tag.
1471     * Uses the ATagParams property.
1472     *
1473     * @param array $conf TypoScript configuration properties
1474     * @param bool|int $addGlobal If set, will add the global config.ATagParams to the link
1475     * @return string String containing the parameters to the A tag (if non empty, with a leading space)
1476     * @see IMGTEXT(), filelink(), makelinks(), typolink()
1477     */
1478    public function getATagParams($conf, $addGlobal = 1)
1479    {
1480        $aTagParams = '';
1481        if ($conf['ATagParams.'] ?? false) {
1482            $aTagParams = ' ' . $this->stdWrap($conf['ATagParams'], $conf['ATagParams.']);
1483        } elseif ($conf['ATagParams'] ?? false) {
1484            $aTagParams = ' ' . $conf['ATagParams'];
1485        }
1486        if ($addGlobal) {
1487            $aTagParams = ' ' . trim($this->getTypoScriptFrontendController()->ATagParams . $aTagParams);
1488        }
1489        // Extend params
1490        $_params = [
1491            'conf' => &$conf,
1492            'aTagParams' => &$aTagParams
1493        ];
1494        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ?? [] as $className) {
1495            $processor = GeneralUtility::makeInstance($className);
1496            $aTagParams = $processor->process($_params, $this);
1497        }
1498
1499        $aTagParams = trim($aTagParams);
1500        if (!empty($aTagParams)) {
1501            $aTagParams = ' ' . $aTagParams;
1502        }
1503
1504        return $aTagParams;
1505    }
1506
1507    /**
1508     * All extension links should ask this function for additional properties to their tags.
1509     * Designed to add for instance an "onclick" property for site tracking systems.
1510     *
1511     * @param string $URL URL of the website
1512     * @param string $TYPE
1513     * @return string The additional tag properties
1514     */
1515    public function extLinkATagParams($URL, $TYPE)
1516    {
1517        $out = '';
1518        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['extLinkATagParamsHandler'])) {
1519            $extLinkATagParamsHandler = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['extLinkATagParamsHandler']);
1520            if (method_exists($extLinkATagParamsHandler, 'main')) {
1521                $out .= trim($extLinkATagParamsHandler->main($URL, $TYPE, $this));
1522            }
1523        }
1524        return trim($out) ? ' ' . trim($out) : '';
1525    }
1526
1527    /***********************************************
1528     *
1529     * HTML template processing functions
1530     *
1531     ***********************************************/
1532
1533    /**
1534     * Sets the current file object during iterations over files.
1535     *
1536     * @param File $fileObject The file object.
1537     */
1538    public function setCurrentFile($fileObject)
1539    {
1540        $this->currentFile = $fileObject;
1541    }
1542
1543    /**
1544     * Gets the current file object during iterations over files.
1545     *
1546     * @return File The current file object.
1547     */
1548    public function getCurrentFile()
1549    {
1550        return $this->currentFile;
1551    }
1552
1553    /***********************************************
1554     *
1555     * "stdWrap" + sub functions
1556     *
1557     ***********************************************/
1558    /**
1559     * The "stdWrap" function. This is the implementation of what is known as "stdWrap properties" in TypoScript.
1560     * Basically "stdWrap" performs some processing of a value based on properties in the input $conf array(holding the TypoScript "stdWrap properties")
1561     * 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.
1562     *
1563     * If $this->alternativeData is an array it's used instead of the $this->data array in ->getData
1564     *
1565     * @param string $content Input value undergoing processing in this function. Possibly substituted by other values fetched from another source.
1566     * @param array $conf TypoScript "stdWrap properties".
1567     * @return string The processed input value
1568     */
1569    public function stdWrap($content = '', $conf = [])
1570    {
1571        $content = (string)$content;
1572        // If there is any hook object, activate all of the process and override functions.
1573        // The hook interface ContentObjectStdWrapHookInterface takes care that all 4 methods exist.
1574        if ($this->stdWrapHookObjects) {
1575            $conf['stdWrapPreProcess'] = 1;
1576            $conf['stdWrapOverride'] = 1;
1577            $conf['stdWrapProcess'] = 1;
1578            $conf['stdWrapPostProcess'] = 1;
1579        }
1580
1581        if (!is_array($conf) || !$conf) {
1582            return $content;
1583        }
1584
1585        // Cache handling
1586        if (isset($conf['cache.']) && is_array($conf['cache.'])) {
1587            $conf['cache.']['key'] = $this->stdWrap($conf['cache.']['key'], $conf['cache.']['key.']);
1588            $conf['cache.']['tags'] = $this->stdWrap($conf['cache.']['tags'], $conf['cache.']['tags.']);
1589            $conf['cache.']['lifetime'] = $this->stdWrap($conf['cache.']['lifetime'], $conf['cache.']['lifetime.']);
1590            $conf['cacheRead'] = 1;
1591            $conf['cacheStore'] = 1;
1592        }
1593        // The configuration is sorted and filtered by intersection with the defined stdWrapOrder.
1594        $sortedConf = array_keys(array_intersect_key($this->stdWrapOrder, $conf));
1595        // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions
1596        $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap';
1597        // Additional Array to check whether a function has already been executed
1598        $isExecuted = [];
1599        // Additional switch to make sure 'required', 'if' and 'fieldRequired'
1600        // will still stop rendering immediately in case they return FALSE
1601        $this->stdWrapRecursionLevel++;
1602        $this->stopRendering[$this->stdWrapRecursionLevel] = false;
1603        // execute each function in the predefined order
1604        foreach ($sortedConf as $stdWrapName) {
1605            // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped
1606            if ((!isset($isExecuted[$stdWrapName]) || !$isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapRecursionLevel]) {
1607                $functionName = rtrim($stdWrapName, '.');
1608                $functionProperties = $functionName . '.';
1609                $functionType = $this->stdWrapOrder[$functionName] ?? null;
1610                // If there is any code on the next level, check if it contains "official" stdWrap functions
1611                // if yes, execute them first - will make each function stdWrap aware
1612                // so additional stdWrap calls within the functions can be removed, since the result will be the same
1613                if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) {
1614                    if (array_intersect_key($this->stdWrapOrder, $conf[$functionProperties])) {
1615                        // Check if there's already content available before processing
1616                        // any ifEmpty or ifBlank stdWrap properties
1617                        if (($functionName === 'ifBlank' && $content !== '') ||
1618                            ($functionName === 'ifEmpty' && trim($content) !== '')) {
1619                            continue;
1620                        }
1621
1622                        $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties] ?? []);
1623                    }
1624                }
1625                // Check if key is still containing something, since it might have been changed by next level stdWrap before
1626                if ((isset($conf[$functionName]) || $conf[$functionProperties])
1627                    && ($functionType !== 'boolean' || $conf[$functionName])
1628                ) {
1629                    // Get just that part of $conf that is needed for the particular function
1630                    $singleConf = [
1631                        $functionName => $conf[$functionName] ?? null,
1632                        $functionProperties => $conf[$functionProperties] ?? null
1633                    ];
1634                    // Hand over the whole $conf array to the stdWrapHookObjects
1635                    if ($functionType === 'hook') {
1636                        $singleConf = $conf;
1637                    }
1638                    // Add both keys - with and without the dot - to the set of executed functions
1639                    $isExecuted[$functionName] = true;
1640                    $isExecuted[$functionProperties] = true;
1641                    // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array
1642                    $functionName = 'stdWrap_' . $functionName;
1643                    $content = $this->{$functionName}($content, $singleConf);
1644                } elseif ($functionType === 'boolean' && !$conf[$functionName]) {
1645                    $isExecuted[$functionName] = true;
1646                    $isExecuted[$functionProperties] = true;
1647                }
1648            }
1649        }
1650        unset($this->stopRendering[$this->stdWrapRecursionLevel]);
1651        $this->stdWrapRecursionLevel--;
1652
1653        return $content;
1654    }
1655
1656    /**
1657     * Gets a configuration value by passing them through stdWrap first and taking a default value if stdWrap doesn't yield a result.
1658     *
1659     * @param string $key The config variable key (from TS array).
1660     * @param array $config The TypoScript array.
1661     * @param string $defaultValue Optional default value.
1662     * @return string Value of the config variable
1663     */
1664    public function stdWrapValue($key, array $config, $defaultValue = '')
1665    {
1666        if (isset($config[$key])) {
1667            if (!isset($config[$key . '.'])) {
1668                return $config[$key];
1669            }
1670        } elseif (isset($config[$key . '.'])) {
1671            $config[$key] = '';
1672        } else {
1673            return $defaultValue;
1674        }
1675        $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']);
1676        return $stdWrapped ?: $defaultValue;
1677    }
1678
1679    /**
1680     * stdWrap pre process hook
1681     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1682     * this hook will execute functions before any other stdWrap function can modify anything
1683     *
1684     * @param string $content Input value undergoing processing in these functions.
1685     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1686     * @return string The processed input value
1687     */
1688    public function stdWrap_stdWrapPreProcess($content = '', $conf = [])
1689    {
1690        foreach ($this->stdWrapHookObjects as $hookObject) {
1691            /** @var ContentObjectStdWrapHookInterface $hookObject */
1692            $content = $hookObject->stdWrapPreProcess($content, $conf, $this);
1693        }
1694        return $content;
1695    }
1696
1697    /**
1698     * Check if content was cached before (depending on the given cache key)
1699     *
1700     * @param string $content Input value undergoing processing in these functions.
1701     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1702     * @return string The processed input value
1703     */
1704    public function stdWrap_cacheRead($content = '', $conf = [])
1705    {
1706        if (!isset($conf['cache.'])) {
1707            return $content;
1708        }
1709        $result = $this->getFromCache($conf['cache.']);
1710        return $result === false ? $content : $result;
1711    }
1712
1713    /**
1714     * Add tags to page cache (comma-separated list)
1715     *
1716     * @param string $content Input value undergoing processing in these functions.
1717     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1718     * @return string The processed input value
1719     */
1720    public function stdWrap_addPageCacheTags($content = '', $conf = [])
1721    {
1722        $tags = isset($conf['addPageCacheTags.'])
1723            ? $this->stdWrap($conf['addPageCacheTags'], $conf['addPageCacheTags.'])
1724            : $conf['addPageCacheTags'];
1725        if (!empty($tags)) {
1726            $cacheTags = GeneralUtility::trimExplode(',', $tags, true);
1727            $this->getTypoScriptFrontendController()->addCacheTags($cacheTags);
1728        }
1729        return $content;
1730    }
1731
1732    /**
1733     * setContentToCurrent
1734     * actually it just does the contrary: Sets the value of 'current' based on current content
1735     *
1736     * @param string $content Input value undergoing processing in this function.
1737     * @return string The processed input value
1738     */
1739    public function stdWrap_setContentToCurrent($content = '')
1740    {
1741        $this->data[$this->currentValKey] = $content;
1742        return $content;
1743    }
1744
1745    /**
1746     * setCurrent
1747     * Sets the value of 'current' based on the outcome of stdWrap operations
1748     *
1749     * @param string $content Input value undergoing processing in this function.
1750     * @param array $conf stdWrap properties for setCurrent.
1751     * @return string The processed input value
1752     */
1753    public function stdWrap_setCurrent($content = '', $conf = [])
1754    {
1755        $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null;
1756        return $content;
1757    }
1758
1759    /**
1760     * lang
1761     * Translates content based on the language currently used by the FE
1762     *
1763     * @param string $content Input value undergoing processing in this function.
1764     * @param array $conf stdWrap properties for lang.
1765     * @return string The processed input value
1766     */
1767    public function stdWrap_lang($content = '', $conf = [])
1768    {
1769        $request = $GLOBALS['TYPO3_REQUEST'] ?? null;
1770        $siteLanguage = $request ? $request->getAttribute('language') : null;
1771        if ($siteLanguage instanceof SiteLanguage) {
1772            $currentLanguageCode = $siteLanguage->getTypo3Language();
1773        } else {
1774            $tsfe = $this->getTypoScriptFrontendController();
1775            $currentLanguageCode = $tsfe->config['config']['language'] ?? null;
1776        }
1777        if ($currentLanguageCode && isset($conf['lang.'][$currentLanguageCode])) {
1778            $content = $conf['lang.'][$currentLanguageCode];
1779        }
1780        return $content;
1781    }
1782
1783    /**
1784     * data
1785     * Gets content from different sources based on getText functions, makes use of alternativeData, when set
1786     *
1787     * @param string $content Input value undergoing processing in this function.
1788     * @param array $conf stdWrap properties for data.
1789     * @return string The processed input value
1790     */
1791    public function stdWrap_data($content = '', $conf = [])
1792    {
1793        $content = $this->getData($conf['data'], is_array($this->alternativeData) ? $this->alternativeData : $this->data);
1794        // This must be unset directly after
1795        $this->alternativeData = '';
1796        return $content;
1797    }
1798
1799    /**
1800     * field
1801     * Gets content from a DB field
1802     *
1803     * @param string $content Input value undergoing processing in this function.
1804     * @param array $conf stdWrap properties for field.
1805     * @return string The processed input value
1806     */
1807    public function stdWrap_field($content = '', $conf = [])
1808    {
1809        return $this->getFieldVal($conf['field']);
1810    }
1811
1812    /**
1813     * current
1814     * Gets content that has been perviously set as 'current'
1815     * Can be set via setContentToCurrent or setCurrent or will be set automatically i.e. inside the split function
1816     *
1817     * @param string $content Input value undergoing processing in this function.
1818     * @param array $conf stdWrap properties for current.
1819     * @return string The processed input value
1820     */
1821    public function stdWrap_current($content = '', $conf = [])
1822    {
1823        return $this->data[$this->currentValKey];
1824    }
1825
1826    /**
1827     * cObject
1828     * Will replace the content with the value of an official TypoScript cObject
1829     * like TEXT, COA, HMENU
1830     *
1831     * @param string $content Input value undergoing processing in this function.
1832     * @param array $conf stdWrap properties for cObject.
1833     * @return string The processed input value
1834     */
1835    public function stdWrap_cObject($content = '', $conf = [])
1836    {
1837        return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject');
1838    }
1839
1840    /**
1841     * numRows
1842     * Counts the number of returned records of a DB operation
1843     * makes use of select internally
1844     *
1845     * @param string $content Input value undergoing processing in this function.
1846     * @param array $conf stdWrap properties for numRows.
1847     * @return string The processed input value
1848     */
1849    public function stdWrap_numRows($content = '', $conf = [])
1850    {
1851        return $this->numRows($conf['numRows.']);
1852    }
1853
1854    /**
1855     * filelist
1856     * Will create a list of files based on some additional parameters
1857     *
1858     * @param string $content Input value undergoing processing in this function.
1859     * @param array $conf stdWrap properties for filelist.
1860     * @return string The processed input value
1861     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead.
1862     */
1863    public function stdWrap_filelist($content = '', $conf = [])
1864    {
1865        return $this->filelist($conf['filelist'], true);
1866    }
1867
1868    /**
1869     * preUserFunc
1870     * Will execute a user public function before the content will be modified by any other stdWrap function
1871     *
1872     * @param string $content Input value undergoing processing in this function.
1873     * @param array $conf stdWrap properties for preUserFunc.
1874     * @return string The processed input value
1875     */
1876    public function stdWrap_preUserFunc($content = '', $conf = [])
1877    {
1878        return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'], $content);
1879    }
1880
1881    /**
1882     * stdWrap override hook
1883     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1884     * this hook will execute functions on existing content but still before the content gets modified or replaced
1885     *
1886     * @param string $content Input value undergoing processing in these functions.
1887     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1888     * @return string The processed input value
1889     */
1890    public function stdWrap_stdWrapOverride($content = '', $conf = [])
1891    {
1892        foreach ($this->stdWrapHookObjects as $hookObject) {
1893            /** @var ContentObjectStdWrapHookInterface $hookObject */
1894            $content = $hookObject->stdWrapOverride($content, $conf, $this);
1895        }
1896        return $content;
1897    }
1898
1899    /**
1900     * override
1901     * Will override the current value of content with its own value'
1902     *
1903     * @param string $content Input value undergoing processing in this function.
1904     * @param array $conf stdWrap properties for override.
1905     * @return string The processed input value
1906     */
1907    public function stdWrap_override($content = '', $conf = [])
1908    {
1909        if (trim($conf['override'] ?? false)) {
1910            $content = $conf['override'];
1911        }
1912        return $content;
1913    }
1914
1915    /**
1916     * preIfEmptyListNum
1917     * Gets a value off a CSV list before the following ifEmpty check
1918     * 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
1919     *
1920     * @param string $content Input value undergoing processing in this function.
1921     * @param array $conf stdWrap properties for preIfEmptyListNum.
1922     * @return string The processed input value
1923     */
1924    public function stdWrap_preIfEmptyListNum($content = '', $conf = [])
1925    {
1926        return $this->listNum($content, $conf['preIfEmptyListNum'] ?? null, $conf['preIfEmptyListNum.']['splitChar'] ?? null);
1927    }
1928
1929    /**
1930     * ifNull
1931     * Will set content to a replacement value in case the value of content is NULL
1932     *
1933     * @param string|null $content Input value undergoing processing in this function.
1934     * @param array $conf stdWrap properties for ifNull.
1935     * @return string The processed input value
1936     */
1937    public function stdWrap_ifNull($content = '', $conf = [])
1938    {
1939        return $content ?? $conf['ifNull'];
1940    }
1941
1942    /**
1943     * ifEmpty
1944     * Will set content to a replacement value in case the trimmed value of content returns FALSE
1945     * 0 (zero) will be replaced as well
1946     *
1947     * @param string $content Input value undergoing processing in this function.
1948     * @param array $conf stdWrap properties for ifEmpty.
1949     * @return string The processed input value
1950     */
1951    public function stdWrap_ifEmpty($content = '', $conf = [])
1952    {
1953        if (!trim($content)) {
1954            $content = $conf['ifEmpty'];
1955        }
1956        return $content;
1957    }
1958
1959    /**
1960     * ifBlank
1961     * Will set content to a replacement value in case the trimmed value of content has no length
1962     * 0 (zero) will not be replaced
1963     *
1964     * @param string $content Input value undergoing processing in this function.
1965     * @param array $conf stdWrap properties for ifBlank.
1966     * @return string The processed input value
1967     */
1968    public function stdWrap_ifBlank($content = '', $conf = [])
1969    {
1970        if (trim($content) === '') {
1971            $content = $conf['ifBlank'];
1972        }
1973        return $content;
1974    }
1975
1976    /**
1977     * listNum
1978     * Gets a value off a CSV list after ifEmpty check
1979     * Might return an empty value in case the CSV does not contain a value at the position given by listNum
1980     * Use preIfEmptyListNum to avoid that behaviour
1981     *
1982     * @param string $content Input value undergoing processing in this function.
1983     * @param array $conf stdWrap properties for listNum.
1984     * @return string The processed input value
1985     */
1986    public function stdWrap_listNum($content = '', $conf = [])
1987    {
1988        return $this->listNum($content, $conf['listNum'] ?? null, $conf['listNum.']['splitChar'] ?? null);
1989    }
1990
1991    /**
1992     * trim
1993     * Cuts off any whitespace at the beginning and the end of the content
1994     *
1995     * @param string $content Input value undergoing processing in this function.
1996     * @return string The processed input value
1997     */
1998    public function stdWrap_trim($content = '')
1999    {
2000        return trim($content);
2001    }
2002
2003    /**
2004     * strPad
2005     * Will return a string padded left/right/on both sides, based on configuration given as stdWrap properties
2006     *
2007     * @param string $content Input value undergoing processing in this function.
2008     * @param array $conf stdWrap properties for strPad.
2009     * @return string The processed input value
2010     */
2011    public function stdWrap_strPad($content = '', $conf = [])
2012    {
2013        // Must specify a length in conf for this to make sense
2014        $length = 0;
2015        // Padding with space is PHP-default
2016        $padWith = ' ';
2017        // Padding on the right side is PHP-default
2018        $padType = STR_PAD_RIGHT;
2019        if (!empty($conf['strPad.']['length'])) {
2020            $length = isset($conf['strPad.']['length.']) ? $this->stdWrap($conf['strPad.']['length'], $conf['strPad.']['length.']) : $conf['strPad.']['length'];
2021            $length = (int)$length;
2022        }
2023        if (isset($conf['strPad.']['padWith']) && (string)$conf['strPad.']['padWith'] !== '') {
2024            $padWith = isset($conf['strPad.']['padWith.']) ? $this->stdWrap($conf['strPad.']['padWith'], $conf['strPad.']['padWith.']) : $conf['strPad.']['padWith'];
2025        }
2026        if (!empty($conf['strPad.']['type'])) {
2027            $type = isset($conf['strPad.']['type.']) ? $this->stdWrap($conf['strPad.']['type'], $conf['strPad.']['type.']) : $conf['strPad.']['type'];
2028            if (strtolower($type) === 'left') {
2029                $padType = STR_PAD_LEFT;
2030            } elseif (strtolower($type) === 'both') {
2031                $padType = STR_PAD_BOTH;
2032            }
2033        }
2034        return str_pad($content, $length, $padWith, $padType);
2035    }
2036
2037    /**
2038     * stdWrap
2039     * A recursive call of the stdWrap function set
2040     * This enables the user to execute stdWrap functions in another than the predefined order
2041     * It modifies the content, not the property
2042     * while the new feature of chained stdWrap functions modifies the property and not the content
2043     *
2044     * @param string $content Input value undergoing processing in this function.
2045     * @param array $conf stdWrap properties for stdWrap.
2046     * @return string The processed input value
2047     */
2048    public function stdWrap_stdWrap($content = '', $conf = [])
2049    {
2050        return $this->stdWrap($content, $conf['stdWrap.']);
2051    }
2052
2053    /**
2054     * stdWrap process hook
2055     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2056     * this hook executes functions directly after the recursive stdWrap function call but still before the content gets modified
2057     *
2058     * @param string $content Input value undergoing processing in these functions.
2059     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2060     * @return string The processed input value
2061     */
2062    public function stdWrap_stdWrapProcess($content = '', $conf = [])
2063    {
2064        foreach ($this->stdWrapHookObjects as $hookObject) {
2065            /** @var ContentObjectStdWrapHookInterface $hookObject */
2066            $content = $hookObject->stdWrapProcess($content, $conf, $this);
2067        }
2068        return $content;
2069    }
2070
2071    /**
2072     * required
2073     * Will immediately stop rendering and return an empty value
2074     * when there is no content at this point
2075     *
2076     * @param string $content Input value undergoing processing in this function.
2077     * @return string The processed input value
2078     */
2079    public function stdWrap_required($content = '')
2080    {
2081        if ((string)$content === '') {
2082            $content = '';
2083            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
2084        }
2085        return $content;
2086    }
2087
2088    /**
2089     * if
2090     * Will immediately stop rendering and return an empty value
2091     * when the result of the checks returns FALSE
2092     *
2093     * @param string $content Input value undergoing processing in this function.
2094     * @param array $conf stdWrap properties for if.
2095     * @return string The processed input value
2096     */
2097    public function stdWrap_if($content = '', $conf = [])
2098    {
2099        if (empty($conf['if.']) || $this->checkIf($conf['if.'])) {
2100            return $content;
2101        }
2102        $this->stopRendering[$this->stdWrapRecursionLevel] = true;
2103        return '';
2104    }
2105
2106    /**
2107     * fieldRequired
2108     * Will immediately stop rendering and return an empty value
2109     * when there is no content in the field given by fieldRequired
2110     *
2111     * @param string $content Input value undergoing processing in this function.
2112     * @param array $conf stdWrap properties for fieldRequired.
2113     * @return string The processed input value
2114     */
2115    public function stdWrap_fieldRequired($content = '', $conf = [])
2116    {
2117        if (!trim($this->data[$conf['fieldRequired'] ?? null] ?? '')) {
2118            $content = '';
2119            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
2120        }
2121        return $content;
2122    }
2123
2124    /**
2125     * stdWrap csConv: Converts the input to UTF-8
2126     *
2127     * The character set of the input must be specified. Returns the input if
2128     * matters go wrong, for example if an invalid character set is given.
2129     *
2130     * @param string $content The string to convert.
2131     * @param array $conf stdWrap properties for csConv.
2132     * @return string The processed input.
2133     */
2134    public function stdWrap_csConv($content = '', $conf = [])
2135    {
2136        if (!empty($conf['csConv'])) {
2137            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv'])));
2138            return $output !== false && $output !== '' ? $output : $content;
2139        }
2140        return $content;
2141    }
2142
2143    /**
2144     * parseFunc
2145     * Will parse the content based on functions given as stdWrap properties
2146     * Heavily used together with RTE based content
2147     *
2148     * @param string $content Input value undergoing processing in this function.
2149     * @param array $conf stdWrap properties for parseFunc.
2150     * @return string The processed input value
2151     */
2152    public function stdWrap_parseFunc($content = '', $conf = [])
2153    {
2154        return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']);
2155    }
2156
2157    /**
2158     * HTMLparser
2159     * Will parse HTML content based on functions given as stdWrap properties
2160     * Heavily used together with RTE based content
2161     *
2162     * @param string $content Input value undergoing processing in this function.
2163     * @param array $conf stdWrap properties for HTMLparser.
2164     * @return string The processed input value
2165     */
2166    public function stdWrap_HTMLparser($content = '', $conf = [])
2167    {
2168        if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) {
2169            $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']);
2170        }
2171        return $content;
2172    }
2173
2174    /**
2175     * split
2176     * Will split the content by a given token and treat the results separately
2177     * Automatically fills 'current' with a single result
2178     *
2179     * @param string $content Input value undergoing processing in this function.
2180     * @param array $conf stdWrap properties for split.
2181     * @return string The processed input value
2182     */
2183    public function stdWrap_split($content = '', $conf = [])
2184    {
2185        return $this->splitObj($content, $conf['split.']);
2186    }
2187
2188    /**
2189     * replacement
2190     * Will execute replacements on the content (optionally with preg-regex)
2191     *
2192     * @param string $content Input value undergoing processing in this function.
2193     * @param array $conf stdWrap properties for replacement.
2194     * @return string The processed input value
2195     */
2196    public function stdWrap_replacement($content = '', $conf = [])
2197    {
2198        return $this->replacement($content, $conf['replacement.']);
2199    }
2200
2201    /**
2202     * prioriCalc
2203     * Will use the content as a mathematical term and calculate the result
2204     * Can be set to 1 to just get a calculated value or 'intval' to get the integer of the result
2205     *
2206     * @param string $content Input value undergoing processing in this function.
2207     * @param array $conf stdWrap properties for prioriCalc.
2208     * @return string The processed input value
2209     */
2210    public function stdWrap_prioriCalc($content = '', $conf = [])
2211    {
2212        $content = MathUtility::calculateWithParentheses($content);
2213        if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') {
2214            $content = (int)$content;
2215        }
2216        return $content;
2217    }
2218
2219    /**
2220     * char
2221     * Returns a one-character string containing the character specified by ascii code.
2222     *
2223     * Reliable results only for character codes in the integer range 0 - 127.
2224     *
2225     * @see http://php.net/manual/en/function.chr.php
2226     * @param string $content Input value undergoing processing in this function.
2227     * @param array $conf stdWrap properties for char.
2228     * @return string The processed input value
2229     */
2230    public function stdWrap_char($content = '', $conf = [])
2231    {
2232        return chr((int)$conf['char']);
2233    }
2234
2235    /**
2236     * intval
2237     * Will return an integer value of the current content
2238     *
2239     * @param string $content Input value undergoing processing in this function.
2240     * @return string The processed input value
2241     */
2242    public function stdWrap_intval($content = '')
2243    {
2244        return (int)$content;
2245    }
2246
2247    /**
2248     * Will return a hashed value of the current content
2249     *
2250     * @param string $content Input value undergoing processing in this function.
2251     * @param array $conf stdWrap properties for hash.
2252     * @return string The processed input value
2253     * @link http://php.net/manual/de/function.hash-algos.php for a list of supported hash algorithms
2254     */
2255    public function stdWrap_hash($content = '', array $conf = [])
2256    {
2257        $algorithm = isset($conf['hash.']) ? $this->stdWrap($conf['hash'], $conf['hash.']) : $conf['hash'];
2258        if (function_exists('hash') && in_array($algorithm, hash_algos())) {
2259            return hash($algorithm, $content);
2260        }
2261        // Non-existing hashing algorithm
2262        return '';
2263    }
2264
2265    /**
2266     * stdWrap_round will return a rounded number with ceil(), floor() or round(), defaults to round()
2267     * Only the english number format is supported . (dot) as decimal point
2268     *
2269     * @param string $content Input value undergoing processing in this function.
2270     * @param array $conf stdWrap properties for round.
2271     * @return string The processed input value
2272     */
2273    public function stdWrap_round($content = '', $conf = [])
2274    {
2275        return $this->round($content, $conf['round.']);
2276    }
2277
2278    /**
2279     * numberFormat
2280     * Will return a formatted number based on configuration given as stdWrap properties
2281     *
2282     * @param string $content Input value undergoing processing in this function.
2283     * @param array $conf stdWrap properties for numberFormat.
2284     * @return string The processed input value
2285     */
2286    public function stdWrap_numberFormat($content = '', $conf = [])
2287    {
2288        return $this->numberFormat($content, $conf['numberFormat.'] ?? []);
2289    }
2290
2291    /**
2292     * expandList
2293     * Will return a formatted number based on configuration given as stdWrap properties
2294     *
2295     * @param string $content Input value undergoing processing in this function.
2296     * @return string The processed input value
2297     */
2298    public function stdWrap_expandList($content = '')
2299    {
2300        return GeneralUtility::expandList($content);
2301    }
2302
2303    /**
2304     * date
2305     * Will return a formatted date based on configuration given according to PHP date/gmdate properties
2306     * Will return gmdate when the property GMT returns TRUE
2307     *
2308     * @param string $content Input value undergoing processing in this function.
2309     * @param array $conf stdWrap properties for date.
2310     * @return string The processed input value
2311     */
2312    public function stdWrap_date($content = '', $conf = [])
2313    {
2314        // Check for zero length string to mimic default case of date/gmdate.
2315        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
2316        $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
2317        return $content;
2318    }
2319
2320    /**
2321     * strftime
2322     * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties
2323     * Will return gmstrftime when the property GMT returns TRUE
2324     *
2325     * @param string $content Input value undergoing processing in this function.
2326     * @param array $conf stdWrap properties for strftime.
2327     * @return string The processed input value
2328     */
2329    public function stdWrap_strftime($content = '', $conf = [])
2330    {
2331        // Check for zero length string to mimic default case of strtime/gmstrftime
2332        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
2333        $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT'])
2334            ? gmstrftime($conf['strftime'] ?? null, $content)
2335            : strftime($conf['strftime'] ?? null, $content);
2336        if (!empty($conf['strftime.']['charset'])) {
2337            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['strftime.']['charset'])));
2338            return $output ?: $content;
2339        }
2340        return $content;
2341    }
2342
2343    /**
2344     * strtotime
2345     * Will return a timestamp based on configuration given according to PHP strtotime
2346     *
2347     * @param string $content Input value undergoing processing in this function.
2348     * @param array $conf stdWrap properties for strtotime.
2349     * @return string The processed input value
2350     */
2351    public function stdWrap_strtotime($content = '', $conf = [])
2352    {
2353        if ($conf['strtotime'] !== '1') {
2354            $content .= ' ' . $conf['strtotime'];
2355        }
2356        return strtotime($content, $GLOBALS['EXEC_TIME']);
2357    }
2358
2359    /**
2360     * age
2361     * Will return the age of a given timestamp based on configuration given by stdWrap properties
2362     *
2363     * @param string $content Input value undergoing processing in this function.
2364     * @param array $conf stdWrap properties for age.
2365     * @return string The processed input value
2366     */
2367    public function stdWrap_age($content = '', $conf = [])
2368    {
2369        return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null);
2370    }
2371
2372    /**
2373     * case
2374     * Will transform the content to be upper or lower case only
2375     * Leaves HTML tags untouched
2376     *
2377     * @param string $content Input value undergoing processing in this function.
2378     * @param array $conf stdWrap properties for case.
2379     * @return string The processed input value
2380     */
2381    public function stdWrap_case($content = '', $conf = [])
2382    {
2383        return $this->HTMLcaseshift($content, $conf['case']);
2384    }
2385
2386    /**
2387     * bytes
2388     * Will return the size of a given number in Bytes	 *
2389     *
2390     * @param string $content Input value undergoing processing in this function.
2391     * @param array $conf stdWrap properties for bytes.
2392     * @return string The processed input value
2393     */
2394    public function stdWrap_bytes($content = '', $conf = [])
2395    {
2396        return GeneralUtility::formatSize($content, $conf['bytes.']['labels'], $conf['bytes.']['base']);
2397    }
2398
2399    /**
2400     * substring
2401     * Will return a substring based on position information given by stdWrap properties
2402     *
2403     * @param string $content Input value undergoing processing in this function.
2404     * @param array $conf stdWrap properties for substring.
2405     * @return string The processed input value
2406     */
2407    public function stdWrap_substring($content = '', $conf = [])
2408    {
2409        return $this->substring($content, $conf['substring']);
2410    }
2411
2412    /**
2413     * cropHTML
2414     * Crops content to a given size while leaving HTML tags untouched
2415     *
2416     * @param string $content Input value undergoing processing in this function.
2417     * @param array $conf stdWrap properties for cropHTML.
2418     * @return string The processed input value
2419     */
2420    public function stdWrap_cropHTML($content = '', $conf = [])
2421    {
2422        return $this->cropHTML($content, $conf['cropHTML'] ?? '');
2423    }
2424
2425    /**
2426     * stripHtml
2427     * Copmletely removes HTML tags from content
2428     *
2429     * @param string $content Input value undergoing processing in this function.
2430     * @return string The processed input value
2431     */
2432    public function stdWrap_stripHtml($content = '')
2433    {
2434        return strip_tags($content);
2435    }
2436
2437    /**
2438     * crop
2439     * Crops content to a given size without caring about HTML tags
2440     *
2441     * @param string $content Input value undergoing processing in this function.
2442     * @param array $conf stdWrap properties for crop.
2443     * @return string The processed input value
2444     */
2445    public function stdWrap_crop($content = '', $conf = [])
2446    {
2447        return $this->crop($content, $conf['crop']);
2448    }
2449
2450    /**
2451     * rawUrlEncode
2452     * Encodes content to be used within URLs
2453     *
2454     * @param string $content Input value undergoing processing in this function.
2455     * @return string The processed input value
2456     */
2457    public function stdWrap_rawUrlEncode($content = '')
2458    {
2459        return rawurlencode($content);
2460    }
2461
2462    /**
2463     * htmlSpecialChars
2464     * Transforms HTML tags to readable text by replacing special characters with their HTML entity
2465     * When preserveEntities returns TRUE, existing entities will be left untouched
2466     *
2467     * @param string $content Input value undergoing processing in this function.
2468     * @param array $conf stdWrap properties for htmlSpecalChars.
2469     * @return string The processed input value
2470     */
2471    public function stdWrap_htmlSpecialChars($content = '', $conf = [])
2472    {
2473        if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) {
2474            $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false);
2475        } else {
2476            $content = htmlspecialchars($content);
2477        }
2478        return $content;
2479    }
2480
2481    /**
2482     * encodeForJavaScriptValue
2483     * Escapes content to be used inside JavaScript strings. Single quotes are added around the value.
2484     *
2485     * @param string $content Input value undergoing processing in this function
2486     * @return string The processed input value
2487     */
2488    public function stdWrap_encodeForJavaScriptValue($content = '')
2489    {
2490        return GeneralUtility::quoteJSvalue($content);
2491    }
2492
2493    /**
2494     * doubleBrTag
2495     * Searches for double line breaks and replaces them with the given value
2496     *
2497     * @param string $content Input value undergoing processing in this function.
2498     * @param array $conf stdWrap properties for doubleBrTag.
2499     * @return string The processed input value
2500     */
2501    public function stdWrap_doubleBrTag($content = '', $conf = [])
2502    {
2503        return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? null, $content);
2504    }
2505
2506    /**
2507     * br
2508     * Searches for single line breaks and replaces them with a <br />/<br> tag
2509     * according to the doctype
2510     *
2511     * @param string $content Input value undergoing processing in this function.
2512     * @return string The processed input value
2513     */
2514    public function stdWrap_br($content = '')
2515    {
2516        return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype));
2517    }
2518
2519    /**
2520     * brTag
2521     * Searches for single line feeds and replaces them with the given value
2522     *
2523     * @param string $content Input value undergoing processing in this function.
2524     * @param array $conf stdWrap properties for brTag.
2525     * @return string The processed input value
2526     */
2527    public function stdWrap_brTag($content = '', $conf = [])
2528    {
2529        return str_replace(LF, $conf['brTag'] ?? null, $content);
2530    }
2531
2532    /**
2533     * encapsLines
2534     * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet
2535     * and wrapping them with values given by stdWrap properties
2536     *
2537     * @param string $content Input value undergoing processing in this function.
2538     * @param array $conf stdWrap properties for erncapsLines.
2539     * @return string The processed input value
2540     */
2541    public function stdWrap_encapsLines($content = '', $conf = [])
2542    {
2543        return $this->encaps_lineSplit($content, $conf['encapsLines.']);
2544    }
2545
2546    /**
2547     * keywords
2548     * Transforms content into a CSV list to be used i.e. as keywords within a meta tag
2549     *
2550     * @param string $content Input value undergoing processing in this function.
2551     * @return string The processed input value
2552     */
2553    public function stdWrap_keywords($content = '')
2554    {
2555        return $this->keywords($content);
2556    }
2557
2558    /**
2559     * innerWrap
2560     * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2561     * See wrap
2562     *
2563     * @param string $content Input value undergoing processing in this function.
2564     * @param array $conf stdWrap properties for innerWrap.
2565     * @return string The processed input value
2566     */
2567    public function stdWrap_innerWrap($content = '', $conf = [])
2568    {
2569        return $this->wrap($content, $conf['innerWrap'] ?? null);
2570    }
2571
2572    /**
2573     * innerWrap2
2574     * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2575     * See wrap
2576     *
2577     * @param string $content Input value undergoing processing in this function.
2578     * @param array $conf stdWrap properties for innerWrap2.
2579     * @return string The processed input value
2580     */
2581    public function stdWrap_innerWrap2($content = '', $conf = [])
2582    {
2583        return $this->wrap($content, $conf['innerWrap2'] ?? null);
2584    }
2585
2586    /**
2587     * addParams
2588     * Adds tag attributes to any content that is a tag
2589     *
2590     * @param string $content Input value undergoing processing in this function.
2591     * @param array $conf stdWrap properties for addParams.
2592     * @return string The processed input value
2593     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0.
2594     */
2595    public function stdWrap_addParams($content = '', $conf = [])
2596    {
2597        return $this->addParams($content, $conf['addParams.'] ?? [], true);
2598    }
2599
2600    /**
2601     * filelink
2602     * Used to make lists of links to files
2603     * See wrap
2604     *
2605     * @param string $content Input value undergoing processing in this function.
2606     * @param array $conf stdWrap properties for filelink.
2607     * @return string The processed input value
2608     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead.
2609     */
2610    public function stdWrap_filelink($content = '', $conf = [])
2611    {
2612        return $this->filelink($content, $conf['filelink.'] ?? [], true);
2613    }
2614
2615    /**
2616     * preCObject
2617     * A content object that is prepended to the current content but between the innerWraps and the rest of the wraps
2618     *
2619     * @param string $content Input value undergoing processing in this function.
2620     * @param array $conf stdWrap properties for preCObject.
2621     * @return string The processed input value
2622     */
2623    public function stdWrap_preCObject($content = '', $conf = [])
2624    {
2625        return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content;
2626    }
2627
2628    /**
2629     * postCObject
2630     * A content object that is appended to the current content but between the innerWraps and the rest of the wraps
2631     *
2632     * @param string $content Input value undergoing processing in this function.
2633     * @param array $conf stdWrap properties for postCObject.
2634     * @return string The processed input value
2635     */
2636    public function stdWrap_postCObject($content = '', $conf = [])
2637    {
2638        return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject');
2639    }
2640
2641    /**
2642     * wrapAlign
2643     * Wraps content with a div container having the style attribute text-align set to the given value
2644     * See wrap
2645     *
2646     * @param string $content Input value undergoing processing in this function.
2647     * @param array $conf stdWrap properties for wrapAlign.
2648     * @return string The processed input value
2649     */
2650    public function stdWrap_wrapAlign($content = '', $conf = [])
2651    {
2652        $wrapAlign = trim($conf['wrapAlign'] ?? '');
2653        if ($wrapAlign) {
2654            $content = $this->wrap($content, '<div style="text-align:' . htmlspecialchars($wrapAlign) . ';">|</div>');
2655        }
2656        return $content;
2657    }
2658
2659    /**
2660     * typolink
2661     * Wraps the content with a link tag
2662     * URLs and other attributes are created automatically by the values given in the stdWrap properties
2663     * See wrap
2664     *
2665     * @param string $content Input value undergoing processing in this function.
2666     * @param array $conf stdWrap properties for typolink.
2667     * @return string The processed input value
2668     */
2669    public function stdWrap_typolink($content = '', $conf = [])
2670    {
2671        return $this->typoLink($content, $conf['typolink.']);
2672    }
2673
2674    /**
2675     * wrap
2676     * This is the "mother" of all wraps
2677     * Third of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2678     * Basically it will put additional content before and after the current content using a split character as a placeholder for the current content
2679     * The default split character is | but it can be replaced with other characters by the property splitChar
2680     * Any other wrap that does not have own splitChar settings will be using the default split char though
2681     *
2682     * @param string $content Input value undergoing processing in this function.
2683     * @param array $conf stdWrap properties for wrap.
2684     * @return string The processed input value
2685     */
2686    public function stdWrap_wrap($content = '', $conf = [])
2687    {
2688        return $this->wrap(
2689            $content,
2690            $conf['wrap'] ?? null,
2691            $conf['wrap.']['splitChar'] ?? '|'
2692        );
2693    }
2694
2695    /**
2696     * noTrimWrap
2697     * Fourth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2698     * The major difference to any other wrap is, that this one can make use of whitespace without trimming	 *
2699     *
2700     * @param string $content Input value undergoing processing in this function.
2701     * @param array $conf stdWrap properties for noTrimWrap.
2702     * @return string The processed input value
2703     */
2704    public function stdWrap_noTrimWrap($content = '', $conf = [])
2705    {
2706        $splitChar = isset($conf['noTrimWrap.']['splitChar.'])
2707            ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.'])
2708            : $conf['noTrimWrap.']['splitChar'] ?? '';
2709        if ($splitChar === null || $splitChar === '') {
2710            $splitChar = '|';
2711        }
2712        $content = $this->noTrimWrap(
2713            $content,
2714            $conf['noTrimWrap'],
2715            $splitChar
2716        );
2717        return $content;
2718    }
2719
2720    /**
2721     * wrap2
2722     * Fifth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2723     * The default split character is | but it can be replaced with other characters by the property splitChar
2724     *
2725     * @param string $content Input value undergoing processing in this function.
2726     * @param array $conf stdWrap properties for wrap2.
2727     * @return string The processed input value
2728     */
2729    public function stdWrap_wrap2($content = '', $conf = [])
2730    {
2731        return $this->wrap(
2732            $content,
2733            $conf['wrap2'] ?? null,
2734            $conf['wrap2.']['splitChar'] ?? '|'
2735        );
2736    }
2737
2738    /**
2739     * dataWrap
2740     * Sixth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2741     * 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
2742     *
2743     * @param string $content Input value undergoing processing in this function.
2744     * @param array $conf stdWrap properties for dataWrap.
2745     * @return string The processed input value
2746     */
2747    public function stdWrap_dataWrap($content = '', $conf = [])
2748    {
2749        return $this->dataWrap($content, $conf['dataWrap']);
2750    }
2751
2752    /**
2753     * prepend
2754     * A content object that will be prepended to the current content after most of the wraps have already been applied
2755     *
2756     * @param string $content Input value undergoing processing in this function.
2757     * @param array $conf stdWrap properties for prepend.
2758     * @return string The processed input value
2759     */
2760    public function stdWrap_prepend($content = '', $conf = [])
2761    {
2762        return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content;
2763    }
2764
2765    /**
2766     * append
2767     * A content object that will be appended to the current content after most of the wraps have already been applied
2768     *
2769     * @param string $content Input value undergoing processing in this function.
2770     * @param array $conf stdWrap properties for append.
2771     * @return string The processed input value
2772     */
2773    public function stdWrap_append($content = '', $conf = [])
2774    {
2775        return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append');
2776    }
2777
2778    /**
2779     * wrap3
2780     * Seventh of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2781     * The default split character is | but it can be replaced with other characters by the property splitChar
2782     *
2783     * @param string $content Input value undergoing processing in this function.
2784     * @param array $conf stdWrap properties for wrap3.
2785     * @return string The processed input value
2786     */
2787    public function stdWrap_wrap3($content = '', $conf = [])
2788    {
2789        return $this->wrap(
2790            $content,
2791            $conf['wrap3'] ?? null,
2792            $conf['wrap3.']['splitChar'] ?? '|'
2793        );
2794    }
2795
2796    /**
2797     * orderedStdWrap
2798     * Calls stdWrap for each entry in the provided array
2799     *
2800     * @param string $content Input value undergoing processing in this function.
2801     * @param array $conf stdWrap properties for orderedStdWrap.
2802     * @return string The processed input value
2803     */
2804    public function stdWrap_orderedStdWrap($content = '', $conf = [])
2805    {
2806        $sortedKeysArray = ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true);
2807        foreach ($sortedKeysArray as $key) {
2808            $content = $this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null);
2809        }
2810        return $content;
2811    }
2812
2813    /**
2814     * outerWrap
2815     * Eighth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2816     *
2817     * @param string $content Input value undergoing processing in this function.
2818     * @param array $conf stdWrap properties for outerWrap.
2819     * @return string The processed input value
2820     */
2821    public function stdWrap_outerWrap($content = '', $conf = [])
2822    {
2823        return $this->wrap($content, $conf['outerWrap'] ?? null);
2824    }
2825
2826    /**
2827     * insertData
2828     * Can fetch additional content the same way data does and replaces any occurrence of {field:whatever} with this content
2829     *
2830     * @param string $content Input value undergoing processing in this function.
2831     * @return string The processed input value
2832     */
2833    public function stdWrap_insertData($content = '')
2834    {
2835        return $this->insertData($content);
2836    }
2837
2838    /**
2839     * postUserFunc
2840     * Will execute a user function after the content has been modified by any other stdWrap function
2841     *
2842     * @param string $content Input value undergoing processing in this function.
2843     * @param array $conf stdWrap properties for postUserFunc.
2844     * @return string The processed input value
2845     */
2846    public function stdWrap_postUserFunc($content = '', $conf = [])
2847    {
2848        return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'], $content);
2849    }
2850
2851    /**
2852     * postUserFuncInt
2853     * Will execute a user function after the content has been created and each time it is fetched from Cache
2854     * The result of this function itself will not be cached
2855     *
2856     * @param string $content Input value undergoing processing in this function.
2857     * @param array $conf stdWrap properties for postUserFuncInt.
2858     * @return string The processed input value
2859     */
2860    public function stdWrap_postUserFuncInt($content = '', $conf = [])
2861    {
2862        $substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash();
2863        $this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [
2864            'content' => $content,
2865            'postUserFunc' => $conf['postUserFuncInt'],
2866            'conf' => $conf['postUserFuncInt.'],
2867            'type' => 'POSTUSERFUNC',
2868            'cObj' => serialize($this)
2869        ];
2870        $content = '<!--' . $substKey . '-->';
2871        return $content;
2872    }
2873
2874    /**
2875     * prefixComment
2876     * Will add HTML comments to the content to make it easier to identify certain content elements within the HTML output later on
2877     *
2878     * @param string $content Input value undergoing processing in this function.
2879     * @param array $conf stdWrap properties for prefixComment.
2880     * @return string The processed input value
2881     */
2882    public function stdWrap_prefixComment($content = '', $conf = [])
2883    {
2884        if (
2885            (!isset($this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) || !$this->getTypoScriptFrontendController()->config['config']['disablePrefixComment'])
2886            && !empty($conf['prefixComment'])
2887        ) {
2888            $content = $this->prefixComment($conf['prefixComment'], [], $content);
2889        }
2890        return $content;
2891    }
2892
2893    /**
2894     * editIcons
2895     * Will render icons for frontend editing as long as there is a BE user logged in
2896     *
2897     * @param string $content Input value undergoing processing in this function.
2898     * @param array $conf stdWrap properties for editIcons.
2899     * @return string The processed input value
2900     */
2901    public function stdWrap_editIcons($content = '', $conf = [])
2902    {
2903        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn() && $conf['editIcons']) {
2904            if (!isset($conf['editIcons.']) || !is_array($conf['editIcons.'])) {
2905                $conf['editIcons.'] = [];
2906            }
2907            $content = $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']);
2908        }
2909        return $content;
2910    }
2911
2912    /**
2913     * editPanel
2914     * Will render the edit panel for frontend editing as long as there is a BE user logged in
2915     *
2916     * @param string $content Input value undergoing processing in this function.
2917     * @param array $conf stdWrap properties for editPanel.
2918     * @return string The processed input value
2919     */
2920    public function stdWrap_editPanel($content = '', $conf = [])
2921    {
2922        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
2923            $content = $this->editPanel($content, $conf['editPanel.']);
2924        }
2925        return $content;
2926    }
2927
2928    public function stdWrap_htmlSanitize(string $content = '', array $conf = []): string
2929    {
2930        $build = $conf['build'] ?? 'default';
2931        if (class_exists($build) && is_a($build, BuilderInterface::class, true)) {
2932            $builder = GeneralUtility::makeInstance($build);
2933        } else {
2934            $factory = GeneralUtility::makeInstance(SanitizerBuilderFactory::class);
2935            $builder = $factory->build($build);
2936        }
2937        $sanitizer = $builder->build();
2938        $initiator = $this->shallDebug()
2939            ? GeneralUtility::makeInstance(SanitizerInitiator::class, DebugUtility::debugTrail())
2940            : null;
2941        return $sanitizer->sanitize($content, $initiator);
2942    }
2943
2944    /**
2945     * Store content into cache
2946     *
2947     * @param string $content Input value undergoing processing in these functions.
2948     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2949     * @return string The processed input value
2950     */
2951    public function stdWrap_cacheStore($content = '', $conf = [])
2952    {
2953        if (!isset($conf['cache.'])) {
2954            return $content;
2955        }
2956        $key = $this->calculateCacheKey($conf['cache.']);
2957        if (empty($key)) {
2958            return $content;
2959        }
2960        /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
2961        $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
2962        $tags = $this->calculateCacheTags($conf['cache.']);
2963        $lifetime = $this->calculateCacheLifetime($conf['cache.']);
2964        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) {
2965            $params = [
2966                'key' => $key,
2967                'content' => $content,
2968                'lifetime' => $lifetime,
2969                'tags' => $tags
2970            ];
2971            GeneralUtility::callUserFunction($_funcRef, $params, $this);
2972        }
2973        $cacheFrontend->set($key, $content, $tags, $lifetime);
2974        return $content;
2975    }
2976
2977    /**
2978     * stdWrap post process hook
2979     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2980     * this hook executes functions at after the content has been modified by the rest of the stdWrap functions but still before debugging
2981     *
2982     * @param string $content Input value undergoing processing in these functions.
2983     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2984     * @return string The processed input value
2985     */
2986    public function stdWrap_stdWrapPostProcess($content = '', $conf = [])
2987    {
2988        foreach ($this->stdWrapHookObjects as $hookObject) {
2989            /** @var ContentObjectStdWrapHookInterface $hookObject */
2990            $content = $hookObject->stdWrapPostProcess($content, $conf, $this);
2991        }
2992        return $content;
2993    }
2994
2995    /**
2996     * debug
2997     * Will output the content as readable HTML code
2998     *
2999     * @param string $content Input value undergoing processing in this function.
3000     * @return string The processed input value
3001     */
3002    public function stdWrap_debug($content = '')
3003    {
3004        return '<pre>' . htmlspecialchars($content) . '</pre>';
3005    }
3006
3007    /**
3008     * debugFunc
3009     * Will output the content in a debug table
3010     *
3011     * @param string $content Input value undergoing processing in this function.
3012     * @param array $conf stdWrap properties for debugFunc.
3013     * @return string The processed input value
3014     */
3015    public function stdWrap_debugFunc($content = '', $conf = [])
3016    {
3017        debug((int)$conf['debugFunc'] === 2 ? [$content] : $content);
3018        return $content;
3019    }
3020
3021    /**
3022     * debugData
3023     * Will output the data used by the current record in a debug table
3024     *
3025     * @param string $content Input value undergoing processing in this function.
3026     * @return string The processed input value
3027     */
3028    public function stdWrap_debugData($content = '')
3029    {
3030        debug($this->data, '$cObj->data:');
3031        if (is_array($this->alternativeData)) {
3032            debug($this->alternativeData, '$this->alternativeData');
3033        }
3034        return $content;
3035    }
3036
3037    /**
3038     * Returns number of rows selected by the query made by the properties set.
3039     * Implements the stdWrap "numRows" property
3040     *
3041     * @param array $conf TypoScript properties for the property (see link to "numRows")
3042     * @return int The number of rows found by the select
3043     * @internal
3044     * @see stdWrap()
3045     */
3046    public function numRows($conf)
3047    {
3048        $conf['select.']['selectFields'] = 'count(*)';
3049        $statement = $this->exec_getQuery($conf['table'], $conf['select.']);
3050
3051        return (int)$statement->fetchColumn(0);
3052    }
3053
3054    /**
3055     * Exploding a string by the $char value (if integer its an ASCII value) and returning index $listNum
3056     *
3057     * @param string $content String to explode
3058     * @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())
3059     * @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.
3060     * @return string
3061     */
3062    public function listNum($content, $listNum, $char)
3063    {
3064        $char = $char ?: ',';
3065        if (MathUtility::canBeInterpretedAsInteger($char)) {
3066            $char = chr($char);
3067        }
3068        $temp = explode($char, $content);
3069        $last = '' . (count($temp) - 1);
3070        // Take a random item if requested
3071        if ($listNum === 'rand') {
3072            $listNum = rand(0, count($temp) - 1);
3073        }
3074        $index = $this->calc(str_ireplace('last', $last, $listNum));
3075        return $temp[$index];
3076    }
3077
3078    /**
3079     * Compares values together based on the settings in the input TypoScript array and returns the comparison result.
3080     * Implements the "if" function in TYPO3 TypoScript
3081     *
3082     * @param array $conf TypoScript properties defining what to compare
3083     * @return bool
3084     * @see stdWrap(), _parseFunc()
3085     */
3086    public function checkIf($conf)
3087    {
3088        if (!is_array($conf)) {
3089            return true;
3090        }
3091        if (isset($conf['directReturn'])) {
3092            return (bool)$conf['directReturn'];
3093        }
3094        $flag = true;
3095        if (isset($conf['isNull.'])) {
3096            $isNull = $this->stdWrap('', $conf['isNull.']);
3097            if ($isNull !== null) {
3098                $flag = false;
3099            }
3100        }
3101        if (isset($conf['isTrue']) || isset($conf['isTrue.'])) {
3102            $isTrue = isset($conf['isTrue.']) ? trim($this->stdWrap($conf['isTrue'], $conf['isTrue.'])) : trim($conf['isTrue']);
3103            if (!$isTrue) {
3104                $flag = false;
3105            }
3106        }
3107        if (isset($conf['isFalse']) || isset($conf['isFalse.'])) {
3108            $isFalse = isset($conf['isFalse.']) ? trim($this->stdWrap($conf['isFalse'], $conf['isFalse.'])) : trim($conf['isFalse']);
3109            if ($isFalse) {
3110                $flag = false;
3111            }
3112        }
3113        if (isset($conf['isPositive']) || isset($conf['isPositive.'])) {
3114            $number = isset($conf['isPositive.']) ? $this->calc($this->stdWrap($conf['isPositive'], $conf['isPositive.'])) : $this->calc($conf['isPositive']);
3115            if ($number < 1) {
3116                $flag = false;
3117            }
3118        }
3119        if ($flag) {
3120            $value = isset($conf['value.'])
3121                ? trim($this->stdWrap($conf['value'] ?? '', $conf['value.']))
3122                : trim($conf['value'] ?? '');
3123            if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) {
3124                $number = isset($conf['isGreaterThan.']) ? trim($this->stdWrap($conf['isGreaterThan'], $conf['isGreaterThan.'])) : trim($conf['isGreaterThan']);
3125                if ($number <= $value) {
3126                    $flag = false;
3127                }
3128            }
3129            if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) {
3130                $number = isset($conf['isLessThan.']) ? trim($this->stdWrap($conf['isLessThan'], $conf['isLessThan.'])) : trim($conf['isLessThan']);
3131                if ($number >= $value) {
3132                    $flag = false;
3133                }
3134            }
3135            if (isset($conf['equals']) || isset($conf['equals.'])) {
3136                $number = isset($conf['equals.']) ? trim($this->stdWrap($conf['equals'], $conf['equals.'])) : trim($conf['equals']);
3137                if ($number != $value) {
3138                    $flag = false;
3139                }
3140            }
3141            if (isset($conf['isInList']) || isset($conf['isInList.'])) {
3142                $number = isset($conf['isInList.']) ? trim($this->stdWrap($conf['isInList'], $conf['isInList.'])) : trim($conf['isInList']);
3143                if (!GeneralUtility::inList($value, $number)) {
3144                    $flag = false;
3145                }
3146            }
3147        }
3148        if ($conf['negate'] ?? false) {
3149            $flag = !$flag;
3150        }
3151        return $flag;
3152    }
3153
3154    /**
3155     * Reads a directory for files and returns the filepaths in a string list separated by comma.
3156     * Implements the stdWrap property "filelist"
3157     *
3158     * @param string $data The command which contains information about what files/directory listing to return. See the "filelist" property of stdWrap for details.
3159     * @param bool $isCoreCall if set, the deprecation message is suppressed
3160     * @return string Comma list of files.
3161     * @internal
3162     * @see stdWrap()
3163     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead.
3164     */
3165    public function filelist($data, bool $isCoreCall = false)
3166    {
3167        if (!$isCoreCall) {
3168            trigger_error('ContentObjectRenderer->filelist() will be removed in TYPO3 v10.0. Use cObject FILES instead.', E_USER_DEPRECATED);
3169        }
3170        $data = trim($data);
3171        if ($data === '') {
3172            return '';
3173        }
3174        list($possiblePath, $ext_list, $sorting, $reverse, $useFullPath) = GeneralUtility::trimExplode('|', $data);
3175        // read directory:
3176        // MUST exist!
3177        $path = '';
3178        // proceeds if no '//', '..' or '\' is in the $theFile
3179        if (GeneralUtility::validPathStr($possiblePath)) {
3180            // Removes all dots, slashes and spaces after a path.
3181            $possiblePath = preg_replace('/[\\/\\. ]*$/', '', $possiblePath);
3182            if (!GeneralUtility::isAbsPath($possiblePath) && @is_dir($possiblePath)) {
3183                // Now check if it matches one of the FAL storages
3184                $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
3185                $storages = $storageRepository->findAll();
3186                foreach ($storages as $storage) {
3187                    if ($storage->getDriverType() === 'Local' && $storage->isPublic() && $storage->isOnline()) {
3188                        $folder = $storage->getPublicUrl($storage->getRootLevelFolder(), true);
3189                        if (GeneralUtility::isFirstPartOfStr($possiblePath . '/', $folder)) {
3190                            $path = $possiblePath;
3191                            break;
3192                        }
3193                    }
3194                }
3195            }
3196        }
3197        if (!$path) {
3198            return '';
3199        }
3200        $items = [
3201            'files' => [],
3202            'sorting' => []
3203        ];
3204        $ext_list = strtolower(GeneralUtility::uniqueList($ext_list));
3205        // Read dir:
3206        $d = @dir($path);
3207        if (is_object($d)) {
3208            $count = 0;
3209            while ($entry = $d->read()) {
3210                if ($entry !== '.' && $entry !== '..') {
3211                    // Because of odd PHP-error where <br />-tag is sometimes placed after a filename!!
3212                    $wholePath = $path . '/' . $entry;
3213                    if (file_exists($wholePath) && filetype($wholePath) === 'file') {
3214                        $info = GeneralUtility::split_fileref($wholePath);
3215                        if (!$ext_list || GeneralUtility::inList($ext_list, $info['fileext'])) {
3216                            $items['files'][] = $info['file'];
3217                            switch ($sorting) {
3218                                case 'name':
3219                                    $items['sorting'][] = strtolower($info['file']);
3220                                    break;
3221                                case 'size':
3222                                    $items['sorting'][] = filesize($wholePath);
3223                                    break;
3224                                case 'ext':
3225                                    $items['sorting'][] = $info['fileext'];
3226                                    break;
3227                                case 'date':
3228                                    $items['sorting'][] = filectime($wholePath);
3229                                    break;
3230                                case 'mdate':
3231                                    $items['sorting'][] = filemtime($wholePath);
3232                                    break;
3233                                default:
3234                                    $items['sorting'][] = $count;
3235                            }
3236                            $count++;
3237                        }
3238                    }
3239                }
3240            }
3241            $d->close();
3242        }
3243        // Sort if required
3244        if (!empty($items['sorting'])) {
3245            if (strtolower($reverse) !== 'r') {
3246                asort($items['sorting']);
3247            } else {
3248                arsort($items['sorting']);
3249            }
3250        }
3251        if (!empty($items['files'])) {
3252            // Make list
3253            reset($items['sorting']);
3254            $list_arr = [];
3255            foreach ($items['sorting'] as $key => $v) {
3256                $list_arr[] = $useFullPath ? $path . '/' . $items['files'][$key] : $items['files'][$key];
3257            }
3258            return implode(',', $list_arr);
3259        }
3260        return '';
3261    }
3262
3263    /**
3264     * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser"
3265     * together with the TypoScript options which are first converted from a TS style array
3266     * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class.
3267     *
3268     * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser
3269     * @param array $conf TypoScript properties for the parser. See link.
3270     * @return string Return value.
3271     * @see stdWrap(), \TYPO3\CMS\Core\Html\HtmlParser::HTMLparserConfig(), \TYPO3\CMS\Core\Html\HtmlParser::HTMLcleaner()
3272     */
3273    public function HTMLparser_TSbridge($theValue, $conf)
3274    {
3275        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3276        $htmlParserCfg = $htmlParser->HTMLparserConfig($conf);
3277        return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]);
3278    }
3279
3280    /**
3281     * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes.
3282     *
3283     * @param string $content Input string being wrapped
3284     * @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.
3285     * @return string Output string wrapped in the wrapping value.
3286     * @see insertData(), stdWrap()
3287     */
3288    public function dataWrap($content, $wrap)
3289    {
3290        return $this->wrap($content, $this->insertData($wrap));
3291    }
3292
3293    /**
3294     * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they
3295     * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces.
3296     * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine
3297     * DBAL and is skipped here for later processing.
3298     *
3299     * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with
3300     * the current pages title field value.
3301     *
3302     * @param string $str Input value
3303     * @return string Processed input value
3304     * @see getData(), stdWrap(), dataWrap()
3305     */
3306    public function insertData($str)
3307    {
3308        $inside = 0;
3309        $newVal = '';
3310        $pointer = 0;
3311        $totalLen = strlen($str);
3312        do {
3313            if (!$inside) {
3314                $len = strcspn(substr($str, $pointer), '{');
3315                $newVal .= substr($str, $pointer, $len);
3316                $inside = true;
3317                if (substr($str, $pointer + $len + 1, 1) === '#') {
3318                    $len2 = strcspn(substr($str, $pointer + $len), '}');
3319                    $newVal .= substr($str, $pointer + $len, $len2);
3320                    $len += $len2;
3321                    $inside = false;
3322                }
3323            } else {
3324                $len = strcspn(substr($str, $pointer), '}') + 1;
3325                $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data);
3326                $inside = false;
3327            }
3328            $pointer += $len;
3329        } while ($pointer < $totalLen);
3330        return $newVal;
3331    }
3332
3333    /**
3334     * 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.
3335     * Notice; this function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript.
3336     *
3337     * @param string $str Input value
3338     * @param array $conf TypoScript Configuration (not used at this point.)
3339     * @param string $content The content to wrap the comment around.
3340     * @return string Processed input value
3341     * @see stdWrap()
3342     */
3343    public function prefixComment($str, $conf, $content)
3344    {
3345        if (empty($str)) {
3346            return $content;
3347        }
3348        $parts = explode('|', $str);
3349        $indent = (int)$parts[0];
3350        $comment = htmlspecialchars($this->insertData($parts[1]));
3351        $output = LF
3352            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF
3353            . str_pad('', $indent + 1, "\t") . $content . LF
3354            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF
3355            . str_pad('', $indent + 1, "\t");
3356        return $output;
3357    }
3358
3359    /**
3360     * Implements the stdWrap property "substring" which is basically a TypoScript implementation of the PHP function, substr()
3361     *
3362     * @param string $content The string to perform the operation on
3363     * @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().
3364     * @return string The processed input value.
3365     * @internal
3366     * @see stdWrap()
3367     */
3368    public function substring($content, $options)
3369    {
3370        $options = GeneralUtility::intExplode(',', $options . ',');
3371        if ($options[1]) {
3372            return mb_substr($content, $options[0], $options[1], 'utf-8');
3373        }
3374        return mb_substr($content, $options[0], null, 'utf-8');
3375    }
3376
3377    /**
3378     * 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.
3379     *
3380     * @param string $content The string to perform the operation on
3381     * @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.
3382     * @return string The processed input value.
3383     * @internal
3384     * @see stdWrap()
3385     */
3386    public function crop($content, $options)
3387    {
3388        $options = explode('|', $options);
3389        $chars = (int)$options[0];
3390        $afterstring = trim($options[1] ?? '');
3391        $crop2space = trim($options[2] ?? '');
3392        if ($chars) {
3393            if (mb_strlen($content, 'utf-8') > abs($chars)) {
3394                $truncatePosition = false;
3395                if ($chars < 0) {
3396                    $content = mb_substr($content, $chars, null, 'utf-8');
3397                    if ($crop2space) {
3398                        $truncatePosition = strpos($content, ' ');
3399                    }
3400                    $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content;
3401                } else {
3402                    $content = mb_substr($content, 0, $chars, 'utf-8');
3403                    if ($crop2space) {
3404                        $truncatePosition = strrpos($content, ' ');
3405                    }
3406                    $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring;
3407                }
3408            }
3409        }
3410        return $content;
3411    }
3412
3413    /**
3414     * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length
3415     * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string
3416     * really was cropped.
3417     *
3418     * Compared to stdWrap.crop it respects HTML tags and entities.
3419     *
3420     * @param string $content The string to perform the operation on
3421     * @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.
3422     * @return string The processed input value.
3423     * @internal
3424     * @see stdWrap()
3425     */
3426    public function cropHTML($content, $options)
3427    {
3428        $options = explode('|', $options);
3429        $chars = (int)$options[0];
3430        $absChars = abs($chars);
3431        $replacementForEllipsis = trim($options[1] ?? '');
3432        $crop2space = trim($options[2] ?? '') === '1';
3433        // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
3434        $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';
3435        $tagsRegEx = '
3436			(
3437				(?:
3438					<!--.*?-->					# a comment
3439					|
3440					<canvas[^>]*>.*?</canvas>   # a canvas tag
3441					|
3442					<script[^>]*>.*?</script>   # a script tag
3443					|
3444					<noscript[^>]*>.*?</noscript> # a noscript tag
3445					|
3446					<template[^>]*>.*?</template> # a template tag
3447				)
3448				|
3449				</?(?:' . $tags . ')+			# opening tag (\'<tag\') or closing tag (\'</tag\')
3450				(?:
3451					(?:
3452						(?:
3453							\\s+\\w[\\w-]*		# EITHER spaces, followed by attribute names
3454							(?:
3455								\\s*=?\\s*		# equals
3456								(?>
3457									".*?"		# attribute values in double-quotes
3458									|
3459									\'.*?\'		# attribute values in single-quotes
3460									|
3461									[^\'">\\s]+	# plain attribute values
3462								)
3463							)?
3464						)
3465						|						# OR a single dash (for TYPO3 link tag)
3466						(?:
3467							\\s+-
3468						)
3469					)+\\s*
3470					|							# OR only spaces
3471					\\s*
3472				)
3473				/?>								# closing the tag with \'>\' or \'/>\'
3474			)';
3475        $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
3476        // Reverse array if we are cropping from right.
3477        if ($chars < 0) {
3478            $splittedContent = array_reverse($splittedContent);
3479        }
3480        // Crop the text (chars of tag-blocks are not counted).
3481        $strLen = 0;
3482        // This is the offset of the content item which was cropped.
3483        $croppedOffset = null;
3484        $countSplittedContent = count($splittedContent);
3485        for ($offset = 0; $offset < $countSplittedContent; $offset++) {
3486            if ($offset % 2 === 0) {
3487                $tempContent = $splittedContent[$offset];
3488                $thisStrLen = mb_strlen(html_entity_decode($tempContent, ENT_COMPAT, 'UTF-8'), 'utf-8');
3489                if ($strLen + $thisStrLen > $absChars) {
3490                    $croppedOffset = $offset;
3491                    $cropPosition = $absChars - $strLen;
3492                    // The snippet "&[^&\s;]{2,8};" in the RegEx below represents entities.
3493                    $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)';
3494                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}#uis';
3495                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3496                        $tempContentPlusOneCharacter = $croppedMatch[0];
3497                    } else {
3498                        $tempContentPlusOneCharacter = false;
3499                    }
3500                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}#uis';
3501                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3502                        $tempContent = $croppedMatch[0];
3503                        if ($crop2space && $tempContentPlusOneCharacter !== false) {
3504                            $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}(?=\\s)#uis';
3505                            if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) {
3506                                $tempContent = $croppedMatch[0];
3507                            }
3508                        }
3509                    }
3510                    $splittedContent[$offset] = $tempContent;
3511                    break;
3512                }
3513                $strLen += $thisStrLen;
3514            }
3515        }
3516        // Close cropped tags.
3517        $closingTags = [];
3518        if ($croppedOffset !== null) {
3519            $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
3520            $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
3521            for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
3522                if (substr($splittedContent[$offset], -2) === '/>') {
3523                    // Ignore empty element tags (e.g. <br />).
3524                    continue;
3525                }
3526                preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches);
3527                $tagName = $matches[1] ?? null;
3528                if ($tagName !== null) {
3529                    // Seek for the closing (or opening) tag.
3530                    $countSplittedContent = count($splittedContent);
3531                    for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) {
3532                        preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches);
3533                        $seekingTagName = $matches[1] ?? null;
3534                        if ($tagName === $seekingTagName) {
3535                            // We found a matching tag.
3536                            // Add closing tag only if it occurs after the cropped content item.
3537                            if ($seekingOffset > $croppedOffset) {
3538                                $closingTags[] = $splittedContent[$seekingOffset];
3539                            }
3540                            break;
3541                        }
3542                    }
3543                }
3544            }
3545            // Drop the cropped items of the content array. The $closingTags will be added later on again.
3546            array_splice($splittedContent, $croppedOffset + 1);
3547        }
3548        $splittedContent = array_merge($splittedContent, [
3549            $croppedOffset !== null ? $replacementForEllipsis : ''
3550        ], $closingTags);
3551        // Reverse array once again if we are cropping from the end.
3552        if ($chars < 0) {
3553            $splittedContent = array_reverse($splittedContent);
3554        }
3555        return implode('', $splittedContent);
3556    }
3557
3558    /**
3559     * Implements the TypoScript function "addParams"
3560     *
3561     * @param string $content The string with the HTML tag.
3562     * @param array $conf The TypoScript configuration properties
3563     * @param bool $isCoreCall if set, the deprecation message is suppressed
3564     * @return string The modified string
3565     * @todo Make it XHTML compatible. Will not present "/>" endings of tags right now. Further getting the tagname might fail if it is not separated by a normal space from the attributes.
3566     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0.
3567     */
3568    public function addParams($content, $conf, $isCoreCall = false)
3569    {
3570        if (!$isCoreCall) {
3571            trigger_error('ContentObjectRenderer->addParams() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
3572        }
3573        // For XHTML compliance.
3574        $lowerCaseAttributes = true;
3575        if (!is_array($conf)) {
3576            return $content;
3577        }
3578        $key = 1;
3579        $parts = explode('<', $content);
3580        if (isset($conf['_offset']) && (int)$conf['_offset']) {
3581            $key = (int)$conf['_offset'] < 0 ? count($parts) + (int)$conf['_offset'] : (int)$conf['_offset'];
3582        }
3583        $subparts = explode('>', $parts[$key] ?? '');
3584        if (trim($subparts[0])) {
3585            // Get attributes and name
3586            $attribs = GeneralUtility::get_tag_attributes('<' . $subparts[0] . '>');
3587            list($tagName) = explode(' ', $subparts[0], 2);
3588            // adds/overrides attributes
3589            foreach ($conf as $pkey => $val) {
3590                if (substr($pkey, -1) !== '.' && $pkey[0] !== '_') {
3591                    $tmpVal = isset($conf[$pkey . '.']) ? $this->stdWrap($conf[$pkey], $conf[$pkey . '.']) : (string)$val;
3592                    if ($lowerCaseAttributes) {
3593                        $pkey = strtolower($pkey);
3594                    }
3595                    if ($tmpVal !== '') {
3596                        $attribs[$pkey] = $tmpVal;
3597                    }
3598                }
3599            }
3600            // Re-assembles the tag and content
3601            $subparts[0] = trim($tagName . ' ' . GeneralUtility::implodeAttributes($attribs));
3602            $parts[$key] = implode('>', $subparts);
3603            $content = implode('<', $parts);
3604        }
3605        return $content;
3606    }
3607
3608    /**
3609     * Creates a list of links to files.
3610     * Implements the stdWrap property "filelink"
3611     *
3612     * @param string $theValue The filename to link to, possibly prefixed with $conf[path]
3613     * @param array $conf TypoScript parameters for the TypoScript function ->filelink
3614     * @param bool $isCoreCall if set, the deprecation message is suppressed
3615     * @return string The link to the file possibly with icons, thumbnails, size in bytes shown etc.
3616     * @internal
3617     * @see stdWrap()
3618     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead.
3619     */
3620    public function filelink($theValue, $conf, $isCoreCall = false)
3621    {
3622        if (!$isCoreCall) {
3623            trigger_error('ContentObjectRenderer->filelink() will be removed in TYPO3 v10.0. Use cObject FILES instead.', E_USER_DEPRECATED);
3624        }
3625        $conf['path'] = isset($conf['path.'])
3626            ? $this->stdWrap($conf['path'] ?? '', $conf['path.'])
3627            : ($conf['path'] ?? '');
3628        $theFile = trim($conf['path']) . $theValue;
3629        if (!@is_file($theFile)) {
3630            return '';
3631        }
3632        $theFileEnc = str_replace('%2F', '/', rawurlencode($theFile));
3633        $title = $conf['title'] ?? '';
3634        if (isset($conf['title.'])) {
3635            $title = $this->stdWrap($title, $conf['title.']);
3636        }
3637        $target = $conf['target'] ?? '';
3638        if (isset($conf['target.'])) {
3639            $target = $this->stdWrap($target, $conf['target.']);
3640        }
3641        $tsfe = $this->getTypoScriptFrontendController();
3642
3643        $typoLinkConf = [
3644            'parameter' => $theFileEnc,
3645            'fileTarget' => $target,
3646            'title' => $title,
3647            'ATagParams' => $this->getATagParams($conf)
3648        ];
3649
3650        if (isset($conf['typolinkConfiguration.'])) {
3651            $additionalTypoLinkConfiguration = $conf['typolinkConfiguration.'];
3652            // We only allow additional configuration. This is why the generated conf overwrites the additional conf.
3653            ArrayUtility::mergeRecursiveWithOverrule($additionalTypoLinkConfiguration, $typoLinkConf);
3654            $typoLinkConf = $additionalTypoLinkConfiguration;
3655        }
3656
3657        $theLinkWrap = $this->typoLink('|', $typoLinkConf);
3658        $theSize = filesize($theFile);
3659        $fI = GeneralUtility::split_fileref($theFile);
3660        $icon = '';
3661        if ($conf['icon'] ?? false) {
3662            $conf['icon.']['path'] = isset($conf['icon.']['path.'])
3663                ? $this->stdWrap($conf['icon.']['path'], $conf['icon.']['path.'])
3664                : $conf['icon.']['path'];
3665            $iconPath = !empty($conf['icon.']['path'])
3666                ? $conf['icon.']['path']
3667                : GeneralUtility::getFileAbsFileName('EXT:frontend/Resources/Public/Icons/FileIcons/');
3668            $conf['icon.']['ext'] = isset($conf['icon.']['ext.'])
3669                ? $this->stdWrap($conf['icon.']['ext'], $conf['icon.']['ext.'])
3670                : $conf['icon.']['ext'];
3671            $iconExt = !empty($conf['icon.']['ext']) ? '.' . $conf['icon.']['ext'] : '.gif';
3672            $icon = @is_file($iconPath . $fI['fileext'] . $iconExt)
3673                ? $iconPath . $fI['fileext'] . $iconExt
3674                : $iconPath . 'default' . $iconExt;
3675            $icon = PathUtility::stripPathSitePrefix($icon);
3676            // Checking for images: If image, then return link to thumbnail.
3677            $IEList = isset($conf['icon_image_ext_list.']) ? $this->stdWrap($conf['icon_image_ext_list'], $conf['icon_image_ext_list.']) : $conf['icon_image_ext_list'];
3678            $image_ext_list = str_replace(' ', '', strtolower($IEList));
3679            if ($fI['fileext'] && GeneralUtility::inList($image_ext_list, $fI['fileext'])) {
3680                if ($conf['iconCObject']) {
3681                    $icon = $this->cObjGetSingle($conf['iconCObject'], $conf['iconCObject.'], 'iconCObject');
3682                } else {
3683                    $notFoundThumb = GeneralUtility::getFileAbsFileName('EXT:core/Resources/Public/Images/NotFound.gif');
3684                    $notFoundThumb = PathUtility::stripPathSitePrefix($notFoundThumb);
3685                    $sizeParts = [64, 64];
3686                    if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']) {
3687                        // using the File Abstraction Layer to generate a preview image
3688                        try {
3689                            /** @var File $fileObject */
3690                            $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($theFile);
3691                            if ($fileObject->isMissing()) {
3692                                $icon = $notFoundThumb;
3693                            } else {
3694                                $fileExtension = $fileObject->getExtension();
3695                                if ($fileExtension === 'ttf' || GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileExtension)) {
3696                                    if ($conf['icon_thumbSize'] || $conf['icon_thumbSize.']) {
3697                                        $thumbSize = isset($conf['icon_thumbSize.']) ? $this->stdWrap($conf['icon_thumbSize'], $conf['icon_thumbSize.']) : $conf['icon_thumbSize'];
3698                                        $sizeParts = explode('x', $thumbSize);
3699                                    }
3700                                    $icon = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, [
3701                                        'width' => $sizeParts[0],
3702                                        'height' => $sizeParts[1]
3703                                    ])->getPublicUrl(true);
3704                                }
3705                            }
3706                        } catch (ResourceDoesNotExistException $exception) {
3707                            $icon = $notFoundThumb;
3708                        }
3709                    } else {
3710                        $icon = $notFoundThumb;
3711                    }
3712                    $urlPrefix = '';
3713                    if (parse_url($icon, PHP_URL_HOST) === null) {
3714                        $urlPrefix = $tsfe->absRefPrefix;
3715                    }
3716                    $icon = '<img src="' . htmlspecialchars($urlPrefix . $icon) . '"' .
3717                        ' width="' . (int)$sizeParts[0] . '" height="' . (int)$sizeParts[1] . '" ' .
3718                        $this->getBorderAttr(' border="0"') . '' . $this->getAltParam($conf) . ' />';
3719                }
3720            } else {
3721                $conf['icon.']['widthAttribute'] = isset($conf['icon.']['widthAttribute.'])
3722                    ? $this->stdWrap($conf['icon.']['widthAttribute'], $conf['icon.']['widthAttribute.'])
3723                    : $conf['icon.']['widthAttribute'];
3724                $iconWidth = !empty($conf['icon.']['widthAttribute']) ? $conf['icon.']['widthAttribute'] : 18;
3725                $conf['icon.']['heightAttribute'] = isset($conf['icon.']['heightAttribute.'])
3726                    ? $this->stdWrap($conf['icon.']['heightAttribute'], $conf['icon.']['heightAttribute.'])
3727                    : $conf['icon.']['heightAttribute'];
3728                $iconHeight = !empty($conf['icon.']['heightAttribute']) ? (int)$conf['icon.']['heightAttribute'] : 16;
3729                $icon = '<img src="' . htmlspecialchars($tsfe->absRefPrefix . $icon) . '" width="' . (int)$iconWidth . '" height="' . (int)$iconHeight . '"'
3730                    . $this->getBorderAttr(' border="0"') . $this->getAltParam($conf) . ' />';
3731            }
3732            if ($conf['icon_link'] && !$conf['combinedLink']) {
3733                $icon = $this->wrap($icon, $theLinkWrap);
3734            }
3735            $icon = isset($conf['icon.']) ? $this->stdWrap($icon, $conf['icon.']) : $icon;
3736        }
3737        $size = '';
3738        if ($conf['size'] ?? false) {
3739            $size = isset($conf['size.']) ? $this->stdWrap($theSize, $conf['size.']) : $theSize;
3740        }
3741        // Wrapping file label
3742        if ($conf['removePrependedNumbers'] ?? false) {
3743            $theValue = preg_replace('/_[0-9][0-9](\\.[[:alnum:]]*)$/', '\\1', $theValue);
3744        }
3745        if (isset($conf['labelStdWrap.'])) {
3746            $theValue = $this->stdWrap($theValue, $conf['labelStdWrap.']);
3747        }
3748        // Wrapping file
3749        $wrap = isset($conf['wrap.'])
3750            ? $this->stdWrap($conf['wrap'] ?? '', $conf['wrap.'])
3751            : ($conf['wrap'] ?? '');
3752        if ($conf['combinedLink'] ?? false) {
3753            $theValue = $icon . $theValue;
3754            if ($conf['ATagBeforeWrap']) {
3755                $theValue = $this->wrap($this->wrap($theValue, $wrap), $theLinkWrap);
3756            } else {
3757                $theValue = $this->wrap($this->wrap($theValue, $theLinkWrap), $wrap);
3758            }
3759            $file = isset($conf['file.']) ? $this->stdWrap($theValue, $conf['file.']) : $theValue;
3760            // output
3761            $output = $file . $size;
3762        } else {
3763            if ($conf['ATagBeforeWrap'] ?? false) {
3764                $theValue = $this->wrap($this->wrap($theValue, $wrap), $theLinkWrap);
3765            } else {
3766                $theValue = $this->wrap($this->wrap($theValue, $theLinkWrap), $wrap);
3767            }
3768            $file = isset($conf['file.']) ? $this->stdWrap($theValue, $conf['file.']) : $theValue;
3769            // output
3770            $output = $icon . $file . $size;
3771        }
3772        if (isset($conf['stdWrap.'])) {
3773            $output = $this->stdWrap($output, $conf['stdWrap.']);
3774        }
3775        return $output;
3776    }
3777
3778    /**
3779     * Performs basic mathematical evaluation of the input string. Does NOT take parathesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction())
3780     *
3781     * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
3782     * @return int The result (might be a float if you did a division of the numbers).
3783     * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()
3784     */
3785    public function calc($val)
3786    {
3787        $parts = GeneralUtility::splitCalc($val, '+-*/');
3788        $value = 0;
3789        foreach ($parts as $part) {
3790            $theVal = $part[1];
3791            $sign = $part[0];
3792            if ((string)(int)$theVal === (string)$theVal) {
3793                $theVal = (int)$theVal;
3794            } else {
3795                $theVal = 0;
3796            }
3797            if ($sign === '-') {
3798                $value -= $theVal;
3799            }
3800            if ($sign === '+') {
3801                $value += $theVal;
3802            }
3803            if ($sign === '/') {
3804                if ((int)$theVal) {
3805                    $value /= (int)$theVal;
3806                }
3807            }
3808            if ($sign === '*') {
3809                $value *= $theVal;
3810            }
3811        }
3812        return $value;
3813    }
3814
3815    /**
3816     * This explodes a comma-list into an array where the values are parsed through ContentObjectRender::calc() and cast to (int)(so you are sure to have integers in the output array)
3817     * Used to split and calculate min and max values for GMENUs.
3818     *
3819     * @param string $delim Delimited to explode by
3820     * @param string $string The string with parts in (where each part is evaluated by ->calc())
3821     * @return array And array with evaluated values.
3822     * @see calc(), \TYPO3\CMS\Frontend\ContentObject\Menu\GraphicalMenuContentObject::makeGifs()
3823     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. It is solely used in GMENU, which can be handled there directly.
3824     */
3825    public function calcIntExplode($delim, $string)
3826    {
3827        trigger_error('calcIntExplode will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
3828        $temp = explode($delim, $string);
3829        foreach ($temp as $key => $val) {
3830            $temp[$key] = (int)$this->calc($val);
3831        }
3832        return $temp;
3833    }
3834
3835    /**
3836     * 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.
3837     * 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.
3838     * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse.
3839     *
3840     * @param string $value The string value to explode by $conf[token] and process each part
3841     * @param array $conf TypoScript properties for "split
3842     * @return string Compiled result
3843     * @internal
3844     * @see stdWrap(), \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::procesItemStates()
3845     */
3846    public function splitObj($value, $conf)
3847    {
3848        $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'], $conf['token.']) : $conf['token'];
3849        if ($conf['token'] === '') {
3850            return $value;
3851        }
3852        $valArr = explode($conf['token'], $value);
3853
3854        // return value directly by returnKey. No further processing
3855        if (!empty($valArr) && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))
3856        ) {
3857            $key = isset($conf['returnKey.']) ? (int)$this->stdWrap($conf['returnKey'], $conf['returnKey.']) : (int)$conf['returnKey'];
3858            return $valArr[$key] ?? '';
3859        }
3860
3861        // return the amount of elements. No further processing
3862        if (!empty($valArr) && ($conf['returnCount'] || $conf['returnCount.'])) {
3863            $returnCount = isset($conf['returnCount.']) ? (bool)$this->stdWrap($conf['returnCount'], $conf['returnCount.']) : (bool)$conf['returnCount'];
3864            return $returnCount ? count($valArr) : 0;
3865        }
3866
3867        // calculate splitCount
3868        $splitCount = count($valArr);
3869        $max = isset($conf['max.']) ? (int)$this->stdWrap($conf['max'], $conf['max.']) : (int)$conf['max'];
3870        if ($max && $splitCount > $max) {
3871            $splitCount = $max;
3872        }
3873        $min = isset($conf['min.']) ? (int)$this->stdWrap($conf['min'], $conf['min.']) : (int)$conf['min'];
3874        if ($min && $splitCount < $min) {
3875            $splitCount = $min;
3876        }
3877        $wrap = isset($conf['wrap.']) ? (string)$this->stdWrap($conf['wrap'], $conf['wrap.']) : (string)$conf['wrap'];
3878        $cObjNumSplitConf = isset($conf['cObjNum.']) ? (string)$this->stdWrap($conf['cObjNum'], $conf['cObjNum.']) : (string)$conf['cObjNum'];
3879        $splitArr = [];
3880        if ($wrap !== '' || $cObjNumSplitConf !== '') {
3881            $splitArr['wrap'] = $wrap;
3882            $splitArr['cObjNum'] = $cObjNumSplitConf;
3883            $splitArr = GeneralUtility::makeInstance(TypoScriptService::class)
3884                ->explodeConfigurationForOptionSplit($splitArr, $splitCount);
3885        }
3886        $content = '';
3887        for ($a = 0; $a < $splitCount; $a++) {
3888            $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a;
3889            $value = '' . $valArr[$a];
3890            $this->data[$this->currentValKey] = $value;
3891            if ($splitArr[$a]['cObjNum']) {
3892                $objName = (int)$splitArr[$a]['cObjNum'];
3893                $value = isset($conf[$objName . '.'])
3894                    ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.'])
3895                    : $this->cObjGet($conf[$objName . '.'], $objName . '.');
3896            }
3897            $wrap = isset($splitArr[$a]['wrap.']) ? $this->stdWrap($splitArr[$a]['wrap'], $splitArr[$a]['wrap.']) : $splitArr[$a]['wrap'];
3898            if ($wrap) {
3899                $value = $this->wrap($value, $wrap);
3900            }
3901            $content .= $value;
3902        }
3903        return $content;
3904    }
3905
3906    /**
3907     * Processes ordered replacements on content data.
3908     *
3909     * @param string $content The content to be processed
3910     * @param array $configuration The TypoScript configuration for stdWrap.replacement
3911     * @return string The processed content data
3912     */
3913    protected function replacement($content, array $configuration)
3914    {
3915        // Sorts actions in configuration by numeric index
3916        ksort($configuration, SORT_NUMERIC);
3917        foreach ($configuration as $index => $action) {
3918            // Checks whether we have an valid action and a numeric key ending with a dot ("10.")
3919            if (is_array($action) && substr($index, -1) === '.' && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) {
3920                $content = $this->replacementSingle($content, $action);
3921            }
3922        }
3923        return $content;
3924    }
3925
3926    /**
3927     * Processes a single search/replace on content data.
3928     *
3929     * @param string $content The content to be processed
3930     * @param array $configuration The TypoScript of the search/replace action to be processed
3931     * @return string The processed content data
3932     */
3933    protected function replacementSingle($content, array $configuration)
3934    {
3935        if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) {
3936            // Gets the strings
3937            $search = isset($configuration['search.']) ? $this->stdWrap($configuration['search'], $configuration['search.']) : $configuration['search'];
3938            $replace = isset($configuration['replace.'])
3939                ? $this->stdWrap($configuration['replace'] ?? null, $configuration['replace.'])
3940                : $configuration['replace'] ?? null;
3941            $useRegularExpression = false;
3942            // Determines whether regular expression shall be used
3943            if (isset($configuration['useRegExp'])
3944                || (isset($configuration['useRegExp.']) && $configuration['useRegExp.'])
3945            ) {
3946                $useRegularExpression = isset($configuration['useRegExp.']) ? (bool)$this->stdWrap($configuration['useRegExp'], $configuration['useRegExp.']) : (bool)$configuration['useRegExp'];
3947            }
3948            $useOptionSplitReplace = false;
3949            // Determines whether replace-pattern uses option-split
3950            if (isset($configuration['useOptionSplitReplace']) || isset($configuration['useOptionSplitReplace.'])) {
3951                $useOptionSplitReplace = isset($configuration['useOptionSplitReplace.']) ? (bool)$this->stdWrap($configuration['useOptionSplitReplace'], $configuration['useOptionSplitReplace.']) : (bool)$configuration['useOptionSplitReplace'];
3952            }
3953
3954            // Performs a replacement by preg_replace()
3955            if ($useRegularExpression) {
3956                // Get separator-character which precedes the string and separates search-string from the modifiers
3957                $separator = $search[0];
3958                $startModifiers = strrpos($search, $separator);
3959                if ($separator !== false && $startModifiers > 0) {
3960                    $modifiers = substr($search, $startModifiers + 1);
3961                    // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code
3962                    $modifiers = str_replace('e', '', $modifiers);
3963                    $search = substr($search, 0, $startModifiers + 1) . $modifiers;
3964                }
3965                if ($useOptionSplitReplace) {
3966                    // init for replacement
3967                    $splitCount = preg_match_all($search, $content, $matches);
3968                    $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3969                    $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3970                    $replaceCount = 0;
3971
3972                    $replaceCallback = function ($match) use ($replaceArray, $search, &$replaceCount) {
3973                        $replaceCount++;
3974                        return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]);
3975                    };
3976                    $content = preg_replace_callback($search, $replaceCallback, $content);
3977                } else {
3978                    $content = preg_replace($search, $replace, $content);
3979                }
3980            } elseif ($useOptionSplitReplace) {
3981                // turn search-string into a preg-pattern
3982                $searchPreg = '#' . preg_quote($search, '#') . '#';
3983
3984                // init for replacement
3985                $splitCount = preg_match_all($searchPreg, $content, $matches);
3986                $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3987                $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3988                $replaceCount = 0;
3989
3990                $replaceCallback = function () use ($replaceArray, $search, &$replaceCount) {
3991                    $replaceCount++;
3992                    return $replaceArray[$replaceCount - 1][0];
3993                };
3994                $content = preg_replace_callback($searchPreg, $replaceCallback, $content);
3995            } else {
3996                $content = str_replace($search, $replace, $content);
3997            }
3998        }
3999        return $content;
4000    }
4001
4002    /**
4003     * Implements the "round" property of stdWrap
4004     * This is a Wrapper function for PHP's rounding functions (round,ceil,floor), defaults to round()
4005     *
4006     * @param string $content Value to process
4007     * @param array $conf TypoScript configuration for round
4008     * @return string The formatted number
4009     */
4010    protected function round($content, array $conf = [])
4011    {
4012        $decimals = isset($conf['decimals.'])
4013            ? $this->stdWrap($conf['decimals'] ?? '', $conf['decimals.'])
4014            : ($conf['decimals'] ?? null);
4015        $type = isset($conf['roundType.'])
4016            ? $this->stdWrap($conf['roundType'] ?? '', $conf['roundType.'])
4017            : ($conf['roundType'] ?? null);
4018        $floatVal = (float)$content;
4019        switch ($type) {
4020            case 'ceil':
4021                $content = ceil($floatVal);
4022                break;
4023            case 'floor':
4024                $content = floor($floatVal);
4025                break;
4026            case 'round':
4027
4028            default:
4029                $content = round($floatVal, (int)$decimals);
4030        }
4031        return $content;
4032    }
4033
4034    /**
4035     * Implements the stdWrap property "numberFormat"
4036     * This is a Wrapper function for php's number_format()
4037     *
4038     * @param float $content Value to process
4039     * @param array $conf TypoScript Configuration for numberFormat
4040     * @return string The formatted number
4041     */
4042    public function numberFormat($content, $conf)
4043    {
4044        $decimals = isset($conf['decimals.'])
4045            ? (int)$this->stdWrap($conf['decimals'] ?? '', $conf['decimals.'])
4046            : (int)($conf['decimals'] ?? 0);
4047        $dec_point = isset($conf['dec_point.'])
4048            ? $this->stdWrap($conf['dec_point'] ?? '', $conf['dec_point.'])
4049            : ($conf['dec_point'] ?? null);
4050        $thousands_sep = isset($conf['thousands_sep.'])
4051            ? $this->stdWrap($conf['thousands_sep'] ?? '', $conf['thousands_sep.'])
4052            : ($conf['thousands_sep'] ?? null);
4053        return number_format((float)$content, $decimals, $dec_point, $thousands_sep);
4054    }
4055
4056    /**
4057     * Implements the stdWrap property, "parseFunc".
4058     * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text
4059     * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc.
4060     * It is still a very important function for processing of bodytext which is normally stored in the database
4061     * in a format which is not fully ready to be outputted.
4062     * This situation has not become better by having a RTE around...
4063     *
4064     * This function is actually just splitting the input content according to the configuration of "external blocks".
4065     * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed
4066     * (while other parts/blocks should NOT be parsed).
4067     * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc()
4068     *
4069     * @param string $theValue The value to process.
4070     * @param array $conf TypoScript configuration for parseFunc
4071     * @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!
4072     * @return string The processed value
4073     * @see _parseFunc()
4074     */
4075    public function parseFunc($theValue, $conf, $ref = '')
4076    {
4077        // Fetch / merge reference, if any
4078        if ($ref) {
4079            $temp_conf = [
4080                'parseFunc' => $ref,
4081                'parseFunc.' => $conf
4082            ];
4083            $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc');
4084            $conf = $temp_conf['parseFunc.'];
4085        }
4086        // early return, no processing in case no configuration is given
4087        if (empty($conf)) {
4088            // @deprecated Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0
4089            trigger_error('Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0', E_USER_DEPRECATED);
4090            return $theValue;
4091        }
4092        // Handle HTML sanitizer invocation
4093        if (!isset($conf['htmlSanitize'])) {
4094            // @deprecated Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0
4095            trigger_error('Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0', E_USER_DEPRECATED);
4096            $features = GeneralUtility::makeInstance(Features::class);
4097            $conf['htmlSanitize'] = $features->isFeatureEnabled('security.frontend.htmlSanitizeParseFuncDefault');
4098        }
4099        $conf['htmlSanitize'] = (bool)$conf['htmlSanitize'];
4100
4101        // Process:
4102        if ((string)($conf['externalBlocks'] ?? '') === '') {
4103            $result = $this->_parseFunc($theValue, $conf);
4104            if ($conf['htmlSanitize']) {
4105                $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
4106            }
4107            return $result;
4108        }
4109        $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks'])));
4110        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
4111        $parts = $htmlParser->splitIntoBlock($tags, $theValue);
4112        foreach ($parts as $k => $v) {
4113            if ($k % 2) {
4114                // font:
4115                $tagName = strtolower($htmlParser->getFirstTagName($v));
4116                $cfg = $conf['externalBlocks.'][$tagName . '.'];
4117                if ($cfg['stripNLprev'] || $cfg['stripNL']) {
4118                    $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]);
4119                }
4120                if ($cfg['stripNLnext'] || $cfg['stripNL']) {
4121                    $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]);
4122                }
4123            }
4124        }
4125        foreach ($parts as $k => $v) {
4126            if ($k % 2) {
4127                $tag = $htmlParser->getFirstTag($v);
4128                $tagName = strtolower($htmlParser->getFirstTagName($v));
4129                $cfg = $conf['externalBlocks.'][$tagName . '.'];
4130                if ($cfg['callRecursive']) {
4131                    $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf);
4132                    if (!$cfg['callRecursive.']['dontWrapSelf']) {
4133                        if ($cfg['callRecursive.']['alternativeWrap']) {
4134                            $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']);
4135                        } else {
4136                            if (is_array($cfg['callRecursive.']['tagStdWrap.'])) {
4137                                $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']);
4138                            }
4139                            $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>';
4140                        }
4141                    }
4142                } elseif ($cfg['HTMLtableCells']) {
4143                    $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]);
4144                    foreach ($rowParts as $kk => $vv) {
4145                        if ($kk % 2) {
4146                            $colParts = $htmlParser->splitIntoBlock('td,th', $vv);
4147                            $cc = 0;
4148                            foreach ($colParts as $kkk => $vvv) {
4149                                if ($kkk % 2) {
4150                                    $cc++;
4151                                    $tag = $htmlParser->getFirstTag($vvv);
4152                                    $tagName = strtolower($htmlParser->getFirstTagName($vvv));
4153                                    $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv);
4154                                    if ($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] || !isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && $cfg['HTMLtableCells.']['default.']['callRecursive']) {
4155                                        if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs']) {
4156                                            $colParts[$kkk] = str_replace('</p><p>', '</p>' . LF . '<p>', $colParts[$kkk]);
4157                                        }
4158                                        $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf);
4159                                    }
4160                                    $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'])
4161                                        ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']
4162                                        : $cfg['HTMLtableCells.']['default.']['tagStdWrap.'];
4163                                    if (is_array($tagStdWrap)) {
4164                                        $tag = $this->stdWrap($tag, $tagStdWrap);
4165                                    }
4166                                    $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'])
4167                                        ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']
4168                                        : $cfg['HTMLtableCells.']['default.']['stdWrap.'];
4169                                    if (is_array($stdWrap)) {
4170                                        $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap);
4171                                    }
4172                                    $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>';
4173                                }
4174                            }
4175                            $rowParts[$kk] = implode('', $colParts);
4176                        }
4177                    }
4178                    $parts[$k] = implode('', $rowParts);
4179                }
4180                if (is_array($cfg['stdWrap.'])) {
4181                    $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
4182                }
4183            } else {
4184                $parts[$k] = $this->_parseFunc($parts[$k], $conf);
4185            }
4186        }
4187        $result = implode('', $parts);
4188        if ($conf['htmlSanitize']) {
4189            $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
4190        }
4191        return $result;
4192    }
4193
4194    /**
4195     * Helper function for parseFunc()
4196     *
4197     * @param string $theValue The value to process.
4198     * @param array $conf TypoScript configuration for parseFunc
4199     * @return string The processed value
4200     * @internal
4201     * @see parseFunc()
4202     */
4203    public function _parseFunc($theValue, $conf)
4204    {
4205        if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
4206            return $theValue;
4207        }
4208        // Indicates that the data is from within a tag.
4209        $inside = false;
4210        // Pointer to the total string position
4211        $pointer = 0;
4212        // Loaded with the current typo-tag if any.
4213        $currentTag = '';
4214        $stripNL = 0;
4215        $contentAccum = [];
4216        $contentAccumP = 0;
4217        $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? ''));
4218        $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? ''));
4219        $totalLen = strlen($theValue);
4220        do {
4221            if (!$inside) {
4222                if (!is_array($currentTag)) {
4223                    // These operations should only be performed on code outside the typotags...
4224                    // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/'
4225                    $len_p = 0;
4226                    $c = 100;
4227                    do {
4228                        $len = strcspn(substr($theValue, $pointer + $len_p), '<');
4229                        $len_p += $len + 1;
4230                        $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1)));
4231                        $c--;
4232                    } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47);
4233                    $len = $len_p - 1;
4234                } else {
4235                    // If we're inside a currentTag, just take it to the end of that tag!
4236                    $tempContent = strtolower(substr($theValue, $pointer));
4237                    $len = strpos($tempContent, '</' . $currentTag[0]);
4238                    if (is_string($len) && !$len) {
4239                        $len = strlen($tempContent);
4240                    }
4241                }
4242                // $data is the content until the next <tag-start or end is detected.
4243                // In case of a currentTag set, this would mean all data between the start- and end-tags
4244                $data = substr($theValue, $pointer, $len);
4245                if ($data != '') {
4246                    if ($stripNL) {
4247                        // If the previous tag was set to strip NewLines in the beginning of the next data-chunk.
4248                        $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data);
4249                    }
4250                    // These operations should only be performed on code outside the tags...
4251                    if (!is_array($currentTag)) {
4252                        // Constants
4253                        $tsfe = $this->getTypoScriptFrontendController();
4254                        $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null;
4255                        if ($conf['constants'] && is_array($tmpConstants)) {
4256                            foreach ($tmpConstants as $key => $val) {
4257                                if (is_string($val)) {
4258                                    $data = str_replace('###' . $key . '###', $val, $data);
4259                                }
4260                            }
4261                        }
4262                        // Short
4263                        if (isset($conf['short.']) && is_array($conf['short.'])) {
4264                            $shortWords = $conf['short.'];
4265                            krsort($shortWords);
4266                            foreach ($shortWords as $key => $val) {
4267                                if (is_string($val)) {
4268                                    $data = str_replace($key, $val, $data);
4269                                }
4270                            }
4271                        }
4272                        // stdWrap
4273                        if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) {
4274                            $data = $this->stdWrap($data, $conf['plainTextStdWrap.']);
4275                        }
4276                        // userFunc
4277                        if ($conf['userFunc'] ?? false) {
4278                            $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $data);
4279                        }
4280                        // Makelinks: (Before search-words as we need the links to be generated when searchwords go on...!)
4281                        if ($conf['makelinks'] ?? false) {
4282                            $data = $this->http_makelinks($data, $conf['makelinks.']['http.']);
4283                            $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []);
4284                        }
4285                        // Search Words:
4286                        if ($tsfe->no_cache && $conf['sword'] && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) {
4287                            $newstring = '';
4288                            do {
4289                                $pregSplitMode = 'i';
4290                                if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) {
4291                                    $pregSplitMode = '';
4292                                }
4293                                $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2);
4294                                $newstring .= $pieces[0];
4295                                $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1]));
4296                                $inTag = false;
4297                                if (strstr($pieces[0], '<') || strstr($pieces[0], '>')) {
4298                                    // Returns TRUE, if a '<' is closer to the string-end than '>'.
4299                                    // This is the case if we're INSIDE a tag (that could have been
4300                                    // made by makelinks...) and we must secure, that the inside of a tag is
4301                                    // not marked up.
4302                                    $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>');
4303                                }
4304                                // The searchword:
4305                                $match = substr($data, strlen($pieces[0]), $match_len);
4306                                if (trim($match) && strlen($match) > 1 && !$inTag) {
4307                                    $match = $this->wrap($match, $conf['sword']);
4308                                }
4309                                // Concatenate the Search Word again.
4310                                $newstring .= $match;
4311                                $data = $pieces[1];
4312                            } while ($pieces[1]);
4313                            $data = $newstring;
4314                        }
4315                    }
4316                    $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
4317                        ? $contentAccum[$contentAccumP] . $data
4318                        : $data;
4319                }
4320                $inside = true;
4321            } else {
4322                // tags
4323                $len = strcspn(substr($theValue, $pointer), '>') + 1;
4324                $data = substr($theValue, $pointer, $len);
4325                if (StringUtility::endsWith($data, '/>') && strpos($data, '<link ') !== 0) {
4326                    $tagContent = substr($data, 1, -2);
4327                } else {
4328                    $tagContent = substr($data, 1, -1);
4329                }
4330                $tag = explode(' ', trim($tagContent), 2);
4331                $tag[0] = strtolower($tag[0]);
4332                if ($tag[0][0] === '/') {
4333                    $tag[0] = substr($tag[0], 1);
4334                    $tag['out'] = 1;
4335                }
4336                if ($conf['tags.'][$tag[0]] ?? false) {
4337                    $treated = false;
4338                    $stripNL = false;
4339                    // in-tag
4340                    if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) {
4341                        // $currentTag (array!) is the tag we are currently processing
4342                        $currentTag = $tag;
4343                        $contentAccumP++;
4344                        $treated = true;
4345                        // in-out-tag: img and other empty tags
4346                        if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', $tag[0])) {
4347                            $tag['out'] = 1;
4348                        }
4349                    }
4350                    // out-tag
4351                    if ($currentTag[0] === $tag[0] && isset($tag['out']) && $tag['out']) {
4352                        $theName = $conf['tags.'][$tag[0]];
4353                        $theConf = $conf['tags.'][$tag[0] . '.'];
4354                        // This flag indicates, that NL- (13-10-chars) should be stripped first and last.
4355                        $stripNL = (bool)($theConf['stripNL'] ?? false);
4356                        // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content.
4357                        $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false);
4358                        $this->parameters = [];
4359                        if ($currentTag[1]) {
4360                            // decode HTML entities in attributes, since they're processed
4361                            $params = GeneralUtility::get_tag_attributes($currentTag[1], true);
4362                            if (is_array($params)) {
4363                                foreach ($params as $option => $val) {
4364                                    // contains non-encoded values
4365                                    $this->parameters[strtolower($option)] = $val;
4366                                }
4367                            }
4368                        }
4369                        $this->parameters['allParams'] = trim($currentTag[1]);
4370                        // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer.
4371                        // $stripNL depends on the configuration of the current tag
4372                        if ($stripNL) {
4373                            $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1]);
4374                            $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP]);
4375                            $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP]);
4376                        }
4377                        $this->data[$this->currentValKey] = $contentAccum[$contentAccumP];
4378                        $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]);
4379                        // fetch the content object
4380                        $contentAccum[$contentAccumP] = $newInput;
4381                        $contentAccumP++;
4382                        // If the TypoTag section
4383                        if (!$breakOut) {
4384                            if (!isset($contentAccum[$contentAccumP - 2])) {
4385                                $contentAccum[$contentAccumP - 2] = '';
4386                            }
4387                            $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? '');
4388                            unset($contentAccum[$contentAccumP]);
4389                            unset($contentAccum[$contentAccumP - 1]);
4390                            $contentAccumP -= 2;
4391                        }
4392                        unset($currentTag);
4393                        $treated = true;
4394                    }
4395                    // other tags
4396                    if (!$treated) {
4397                        $contentAccum[$contentAccumP] .= $data;
4398                    }
4399                } else {
4400                    // If a tag was not a typo tag, then it is just added to the content
4401                    $stripNL = false;
4402                    if (GeneralUtility::inList($allowTags, $tag[0]) || $denyTags !== '*' && !GeneralUtility::inList($denyTags, $tag[0])) {
4403                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
4404                            ? $contentAccum[$contentAccumP] . $data
4405                            : $data;
4406                    } else {
4407                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
4408                            ? $contentAccum[$contentAccumP] . htmlspecialchars($data)
4409                            : htmlspecialchars($data);
4410                    }
4411                }
4412                $inside = false;
4413            }
4414            $pointer += $len;
4415        } while ($pointer < $totalLen);
4416        // Parsing nonTypoTag content (all even keys):
4417        reset($contentAccum);
4418        $contentAccumCount = count($contentAccum);
4419        for ($a = 0; $a < $contentAccumCount; $a++) {
4420            if ($a % 2 != 1) {
4421                // stdWrap
4422                if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) {
4423                    $contentAccum[$a] = $this->stdWrap($contentAccum[$a], $conf['nonTypoTagStdWrap.']);
4424                }
4425                // userFunc
4426                if (!empty($conf['nonTypoTagUserFunc'])) {
4427                    $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'], $contentAccum[$a]);
4428                }
4429            }
4430        }
4431        return implode('', $contentAccum);
4432    }
4433
4434    /**
4435     * Lets you split the content by LF and process each line independently. Used to format content made with the RTE.
4436     *
4437     * @param string $theValue The input value
4438     * @param array $conf TypoScript options
4439     * @return string The processed input value being returned; Splitted lines imploded by LF again.
4440     * @internal
4441     */
4442    public function encaps_lineSplit($theValue, $conf)
4443    {
4444        if ((string)$theValue === '') {
4445            return '';
4446        }
4447        $lParts = explode(LF, $theValue);
4448
4449        // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line.
4450        $lastPartIndex = count($lParts) - 1;
4451        if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') {
4452            array_pop($lParts);
4453        }
4454
4455        $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList']), true);
4456        $nonWrappedTag = $conf['nonWrappedTag'];
4457        $defaultAlign = isset($conf['defaultAlign.'])
4458            ? trim($this->stdWrap($conf['defaultAlign'] ?? '', $conf['defaultAlign.']))
4459            : trim($conf['defaultAlign'] ?? '');
4460
4461        $str_content = '';
4462        foreach ($lParts as $k => $l) {
4463            $sameBeginEnd = 0;
4464            $emptyTag = false;
4465            $l = trim($l);
4466            $attrib = [];
4467            $nonWrapped = false;
4468            $tagName = '';
4469            if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') {
4470                $fwParts = explode('>', substr($l, 1), 2);
4471                list($tagName) = explode(' ', $fwParts[0], 2);
4472                if (!$fwParts[1]) {
4473                    if (substr($tagName, -1) === '/') {
4474                        $tagName = substr($tagName, 0, -1);
4475                    }
4476                    if (substr($fwParts[0], -1) === '/') {
4477                        $sameBeginEnd = 1;
4478                        $emptyTag = true;
4479                        // decode HTML entities, they're encoded later again
4480                        $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true);
4481                    }
4482                } else {
4483                    $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2);
4484                    // decode HTML entities, they're encoded later again
4485                    $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true);
4486                    $str_content = $backParts[0];
4487                    $sameBeginEnd = substr(strtolower($backParts[1]), 1, strlen($tagName)) === strtolower($tagName);
4488                }
4489            }
4490            if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) {
4491                $uTagName = strtoupper($tagName);
4492                $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName);
4493            } else {
4494                $uTagName = strtoupper($nonWrappedTag);
4495                // The line will be wrapped: $uTagName should not be an empty tag
4496                $emptyTag = false;
4497                $str_content = $lParts[$k];
4498                $nonWrapped = true;
4499                $attrib = [];
4500            }
4501            // Wrapping all inner-content:
4502            if (is_array($conf['innerStdWrap_all.'])) {
4503                $str_content = $this->stdWrap($str_content, $conf['innerStdWrap_all.']);
4504            }
4505            if ($uTagName) {
4506                // Setting common attributes
4507                if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) {
4508                    foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) {
4509                        if (!is_array($vv)) {
4510                            if ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'blank') {
4511                                if ((string)($attrib[$kk] ?? '') === '') {
4512                                    $attrib[$kk] = $vv;
4513                                }
4514                            } elseif ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'exists') {
4515                                if (!isset($attrib[$kk])) {
4516                                    $attrib[$kk] = $vv;
4517                                }
4518                            } else {
4519                                $attrib[$kk] = $vv;
4520                            }
4521                        }
4522                    }
4523                }
4524                // Wrapping all inner-content:
4525                if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) {
4526                    $str_content = $this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']);
4527                }
4528                // Default align
4529                if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) {
4530                    $attrib['align'] = $defaultAlign;
4531                }
4532                // implode (insecure) attributes, that's why `htmlspecialchars` is used here
4533                $params = GeneralUtility::implodeAttributes($attrib, true);
4534                if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) {
4535                    $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
4536                    if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) {
4537                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />';
4538                    } else {
4539                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>';
4540                    }
4541                }
4542            }
4543            if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) {
4544                $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']);
4545            }
4546            $lParts[$k] = $str_content;
4547        }
4548        return implode(LF, $lParts);
4549    }
4550
4551    /**
4552     * Finds URLS in text and makes it to a real link.
4553     * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link,
4554     * linking to the URL we should have found.
4555     *
4556     * @param string $data The string in which to search for "http://
4557     * @param array $conf Configuration for makeLinks, see link
4558     * @return string The processed input string, being returned.
4559     * @see _parseFunc()
4560     */
4561    public function http_makelinks($data, $conf)
4562    {
4563        $aTagParams = $this->getATagParams($conf);
4564        $textstr = '';
4565        foreach (['http://', 'https://'] as $scheme) {
4566            $textpieces = explode($scheme, $data);
4567            $pieces = count($textpieces);
4568            $textstr = $textpieces[0];
4569            for ($i = 1; $i < $pieces; $i++) {
4570                $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
4571                if (trim(substr($textstr, -1)) === '' && $len) {
4572                    $lastChar = substr($textpieces[$i], $len - 1, 1);
4573                    if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) {
4574                        $len--;
4575                    }
4576                    // Included '\/' 3/12
4577                    $parts[0] = substr($textpieces[$i], 0, $len);
4578                    $parts[1] = substr($textpieces[$i], $len);
4579                    $keep = $conf['keep'];
4580                    $linkParts = parse_url($scheme . $parts[0]);
4581                    $linktxt = '';
4582                    if (strstr($keep, 'scheme')) {
4583                        $linktxt = $scheme;
4584                    }
4585                    $linktxt .= $linkParts['host'];
4586                    if (strstr($keep, 'path')) {
4587                        $linktxt .= $linkParts['path'];
4588                        // Added $linkParts['query'] 3/12
4589                        if (strstr($keep, 'query') && $linkParts['query']) {
4590                            $linktxt .= '?' . $linkParts['query'];
4591                        } elseif ($linkParts['path'] === '/') {
4592                            $linktxt = substr($linktxt, 0, -1);
4593                        }
4594                    }
4595                    if (isset($conf['extTarget'])) {
4596                        if (isset($conf['extTarget.'])) {
4597                            $target = $this->stdWrap($conf['extTarget'], $conf['extTarget.']);
4598                        } else {
4599                            $target = $conf['extTarget'];
4600                        }
4601                    } else {
4602                        $target = $this->getTypoScriptFrontendController()->extTarget;
4603                    }
4604
4605                    // check for jump URLs or similar
4606                    $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf);
4607
4608                    $res = '<a href="' . htmlspecialchars($linkUrl) . '"'
4609                        . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
4610                        . $aTagParams . $this->extLinkATagParams('http://' . $parts[0], 'url') . '>';
4611
4612                    $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
4613                    if ((string)$conf['ATagBeforeWrap'] !== '') {
4614                        $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
4615                    } else {
4616                        $res = $this->wrap($res . $linktxt . '</a>', $wrap);
4617                    }
4618                    $textstr .= $res . $parts[1];
4619                } else {
4620                    $textstr .= $scheme . $textpieces[$i];
4621                }
4622            }
4623            $data = $textstr;
4624        }
4625        return $textstr;
4626    }
4627
4628    /**
4629     * Will find all strings prefixed with "mailto:" in the $data string and make them into a link,
4630     * linking to the email address they point to.
4631     *
4632     * @param string $data The string in which to search for "mailto:
4633     * @param array $conf Configuration for makeLinks, see link
4634     * @return string The processed input string, being returned.
4635     * @see _parseFunc()
4636     */
4637    public function mailto_makelinks($data, $conf)
4638    {
4639        // http-split
4640        $aTagParams = $this->getATagParams($conf);
4641        $textpieces = explode('mailto:', $data);
4642        $pieces = count($textpieces);
4643        $textstr = $textpieces[0];
4644        $tsfe = $this->getTypoScriptFrontendController();
4645        for ($i = 1; $i < $pieces; $i++) {
4646            $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
4647            if (trim(substr($textstr, -1)) === '' && $len) {
4648                $lastChar = substr($textpieces[$i], $len - 1, 1);
4649                if (!preg_match('/[A-Za-z0-9]/', $lastChar)) {
4650                    $len--;
4651                }
4652                $parts[0] = substr($textpieces[$i], 0, $len);
4653                $parts[1] = substr($textpieces[$i], $len);
4654                $linktxt = preg_replace('/\\?.*/', '', $parts[0]);
4655                list($mailToUrl, $linktxt) = $this->getMailTo($parts[0], $linktxt);
4656                $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl);
4657                $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>';
4658                $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
4659                if ((string)$conf['ATagBeforeWrap'] !== '') {
4660                    $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
4661                } else {
4662                    $res = $this->wrap($res . $linktxt . '</a>', $wrap);
4663                }
4664                $textstr .= $res . $parts[1];
4665            } else {
4666                $textstr .= 'mailto:' . $textpieces[$i];
4667            }
4668        }
4669        return $textstr;
4670    }
4671
4672    /**
4673     * Creates and returns a TypoScript "imgResource".
4674     * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER".
4675     * In the first case a current image is returned, possibly scaled down or otherwise processed.
4676     * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines.
4677     * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']);
4678     *
4679     * Structure of the returned info array:
4680     *  0 => width
4681     *  1 => height
4682     *  2 => file extension
4683     *  3 => file name
4684     *  origFile => original file name
4685     *  origFile_mtime => original file mtime
4686     *  -- only available if processed via FAL: --
4687     *  originalFile => original file object
4688     *  processedFile => processed file object
4689     *  fileCacheHash => checksum of processed file
4690     *
4691     * @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.
4692     * @param array $fileArray TypoScript properties for the imgResource type
4693     * @return array|null Returns info-array
4694     * @see IMG_RESOURCE(), cImage(), \TYPO3\CMS\Frontend\Imaging\GifBuilder
4695     */
4696    public function getImgResource($file, $fileArray)
4697    {
4698        if (empty($file) && empty($fileArray)) {
4699            return null;
4700        }
4701        if (!is_array($fileArray)) {
4702            $fileArray = (array)$fileArray;
4703        }
4704        $imageResource = null;
4705        if ($file === 'GIFBUILDER') {
4706            $gifCreator = GeneralUtility::makeInstance(GifBuilder::class);
4707            $theImage = '';
4708            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) {
4709                $gifCreator->start($fileArray, $this->data);
4710                $theImage = $gifCreator->gifBuild();
4711            }
4712            $imageResource = $gifCreator->getImageDimensions($theImage);
4713            $imageResource['origFile'] = $theImage;
4714        } else {
4715            if ($file instanceof File) {
4716                $fileObject = $file;
4717            } elseif ($file instanceof FileReference) {
4718                $fileObject = $file->getOriginalFile();
4719            } else {
4720                try {
4721                    if (isset($fileArray['import.']) && $fileArray['import.']) {
4722                        $importedFile = trim($this->stdWrap('', $fileArray['import.']));
4723                        if (!empty($importedFile)) {
4724                            $file = $importedFile;
4725                        }
4726                    }
4727
4728                    if (MathUtility::canBeInterpretedAsInteger($file)) {
4729                        $treatIdAsReference = isset($fileArray['treatIdAsReference.']) ? $this->stdWrap($fileArray['treatIdAsReference'], $fileArray['treatIdAsReference.']) : $fileArray['treatIdAsReference'];
4730                        if (!empty($treatIdAsReference)) {
4731                            $file = $this->getResourceFactory()->getFileReferenceObject($file);
4732                            $fileObject = $file->getOriginalFile();
4733                        } else {
4734                            $fileObject = $this->getResourceFactory()->getFileObject($file);
4735                        }
4736                    } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier
4737                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4738                    } else {
4739                        if (isset($importedFile) && !empty($importedFile) && !empty($fileArray['import'])) {
4740                            $file = $fileArray['import'] . $file;
4741                        }
4742                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4743                    }
4744                } catch (Exception $exception) {
4745                    $this->logger->warning('The image "' . $file . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]);
4746                    return null;
4747                }
4748            }
4749            if ($fileObject instanceof File) {
4750                $processingConfiguration = [];
4751                $processingConfiguration['width'] = isset($fileArray['width.']) ? $this->stdWrap($fileArray['width'], $fileArray['width.']) : $fileArray['width'];
4752                $processingConfiguration['height'] = isset($fileArray['height.']) ? $this->stdWrap($fileArray['height'], $fileArray['height.']) : $fileArray['height'];
4753                $processingConfiguration['fileExtension'] = isset($fileArray['ext.']) ? $this->stdWrap($fileArray['ext'], $fileArray['ext.']) : $fileArray['ext'];
4754                $processingConfiguration['maxWidth'] = isset($fileArray['maxW.']) ? (int)$this->stdWrap($fileArray['maxW'], $fileArray['maxW.']) : (int)$fileArray['maxW'];
4755                $processingConfiguration['maxHeight'] = isset($fileArray['maxH.']) ? (int)$this->stdWrap($fileArray['maxH'], $fileArray['maxH.']) : (int)$fileArray['maxH'];
4756                $processingConfiguration['minWidth'] = isset($fileArray['minW.']) ? (int)$this->stdWrap($fileArray['minW'], $fileArray['minW.']) : (int)$fileArray['minW'];
4757                $processingConfiguration['minHeight'] = isset($fileArray['minH.']) ? (int)$this->stdWrap($fileArray['minH'], $fileArray['minH.']) : (int)$fileArray['minH'];
4758                $processingConfiguration['noScale'] = isset($fileArray['noScale.']) ? $this->stdWrap($fileArray['noScale'], $fileArray['noScale.']) : $fileArray['noScale'];
4759                $processingConfiguration['additionalParameters'] = isset($fileArray['params.']) ? $this->stdWrap($fileArray['params'], $fileArray['params.']) : $fileArray['params'];
4760                $processingConfiguration['frame'] = isset($fileArray['frame.']) ? (int)$this->stdWrap($fileArray['frame'], $fileArray['frame.']) : (int)$fileArray['frame'];
4761                if ($file instanceof FileReference) {
4762                    $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($file, $fileArray);
4763                } else {
4764                    $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray);
4765                }
4766
4767                // Possibility to cancel/force profile extraction
4768                // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']
4769                if (isset($fileArray['stripProfile'])) {
4770                    $processingConfiguration['stripProfile'] = $fileArray['stripProfile'];
4771                }
4772                // Check if we can handle this type of file for editing
4773                if (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension())) {
4774                    $maskArray = $fileArray['m.'];
4775                    // Must render mask images and include in hash-calculating
4776                    // - otherwise we cannot be sure the filename is unique for the setup!
4777                    if (is_array($maskArray)) {
4778                        $mask = $this->getImgResource($maskArray['mask'], $maskArray['mask.']);
4779                        $bgImg = $this->getImgResource($maskArray['bgImg'], $maskArray['bgImg.']);
4780                        $bottomImg = $this->getImgResource($maskArray['bottomImg'], $maskArray['bottomImg.']);
4781                        $bottomImg_mask = $this->getImgResource($maskArray['bottomImg_mask'], $maskArray['bottomImg_mask.']);
4782
4783                        $processingConfiguration['maskImages']['maskImage'] = $mask['processedFile'];
4784                        $processingConfiguration['maskImages']['backgroundImage'] = $bgImg['processedFile'];
4785                        $processingConfiguration['maskImages']['maskBottomImage'] = $bottomImg['processedFile'];
4786                        $processingConfiguration['maskImages']['maskBottomImageMask'] = $bottomImg_mask['processedFile'];
4787                    }
4788                    $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
4789                    if ($processedFileObject->isProcessed()) {
4790                        $imageResource = [
4791                            0 => (int)$processedFileObject->getProperty('width'),
4792                            1 => (int)$processedFileObject->getProperty('height'),
4793                            2 => $processedFileObject->getExtension(),
4794                            3 => $processedFileObject->getPublicUrl(),
4795                            'origFile' => $fileObject->getPublicUrl(),
4796                            'origFile_mtime' => $fileObject->getModificationTime(),
4797                            // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
4798                            // in order for the setup-array to create a unique filename hash.
4799                            'originalFile' => $fileObject,
4800                            'processedFile' => $processedFileObject
4801                        ];
4802                    }
4803                }
4804            }
4805        }
4806        // If image was processed by GIFBUILDER:
4807        // ($imageResource indicates that it was processed the regular way)
4808        if (!isset($imageResource)) {
4809            try {
4810                $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize((string)$file);
4811                $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB');
4812                $info['origFile'] = $theImage;
4813                // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash.
4814                $info['origFile_mtime'] = @filemtime($theImage);
4815                $imageResource = $info;
4816            } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
4817                // do nothing in case the file path is invalid
4818            }
4819        }
4820        // Hook 'getImgResource': Post-processing of image resources
4821        if (isset($imageResource)) {
4822            /** @var ContentObjectGetImageResourceHookInterface $hookObject */
4823            foreach ($this->getGetImgResourceHookObjects() as $hookObject) {
4824                $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this);
4825            }
4826        }
4827        return $imageResource;
4828    }
4829
4830    /**
4831     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4832     * or null if the crop settings or crop area is empty.
4833     *
4834     * The cropArea from file reference is used, if not set in TypoScript.
4835     *
4836     * Example TypoScript settings:
4837     * file.crop =
4838     * OR
4839     * file.crop = 50,50,100,100
4840     * OR
4841     * file.crop.data = file:current:crop
4842     *
4843     * @param FileReference $fileReference
4844     * @param array $fileArray TypoScript properties for the imgResource type
4845     * @return Area|null
4846     */
4847    protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray)
4848    {
4849        // Use cropping area from file reference if nothing is configured in TypoScript.
4850        if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) {
4851            // Set crop variant from TypoScript settings. If not set, use default.
4852            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4853            $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant);
4854            return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference);
4855        }
4856
4857        return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray);
4858    }
4859
4860    /**
4861     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4862     * or null if the crop settings or crop area is empty.
4863     *
4864     * @param FileInterface $file
4865     * @param array $fileArray
4866     * @return Area|null
4867     */
4868    protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray)
4869    {
4870        /** @var Area $cropArea */
4871        $cropArea = null;
4872        // Resolve TypoScript configured cropping.
4873        $cropSettings = isset($fileArray['crop.'])
4874            ? $this->stdWrap($fileArray['crop'], $fileArray['crop.'])
4875            : ($fileArray['crop'] ?? null);
4876
4877        if (is_string($cropSettings)) {
4878            // Set crop variant from TypoScript settings. If not set, use default.
4879            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4880            // Get cropArea from CropVariantCollection, if cropSettings is a valid json.
4881            // CropVariantCollection::create does json_decode.
4882            $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant);
4883            $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file);
4884
4885            // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100
4886            if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) {
4887                $cropSettings = explode(',', $cropSettings);
4888                if (count($cropSettings) === 4) {
4889                    $stringCropArea = GeneralUtility::makeInstance(
4890                        Area::class,
4891                        ...$cropSettings
4892                    );
4893                    $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea;
4894                }
4895            }
4896        }
4897
4898        return $cropArea;
4899    }
4900
4901    /**
4902     * Takes a JSON string and creates CropVariantCollection and fetches the corresponding
4903     * CropArea for that.
4904     *
4905     * @param string $cropSettings
4906     * @param string $cropVariant
4907     * @return Area
4908     */
4909    protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area
4910    {
4911        return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant);
4912    }
4913
4914    /***********************************************
4915     *
4916     * Data retrieval etc.
4917     *
4918     ***********************************************/
4919    /**
4920     * 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.
4921     *
4922     * @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)
4923     * @return string|null
4924     */
4925    public function getFieldVal($field)
4926    {
4927        if (!strstr($field, '//')) {
4928            return $this->data[trim($field)] ?? null;
4929        }
4930        $sections = GeneralUtility::trimExplode('//', $field, true);
4931        foreach ($sections as $k) {
4932            if ((string)$this->data[$k] !== '') {
4933                return $this->data[$k];
4934            }
4935        }
4936
4937        return '';
4938    }
4939
4940    /**
4941     * 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.
4942     *
4943     * @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)
4944     * @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.
4945     * @return string The value fetched
4946     * @see getFieldVal()
4947     */
4948    public function getData($string, $fieldArray = null)
4949    {
4950        $tsfe = $this->getTypoScriptFrontendController();
4951        if (!is_array($fieldArray)) {
4952            $fieldArray = $tsfe->page;
4953        }
4954        $retVal = '';
4955        $sections = explode('//', $string);
4956        foreach ($sections as $secKey => $secVal) {
4957            if ($retVal) {
4958                break;
4959            }
4960            $parts = explode(':', $secVal, 2);
4961            $type = strtolower(trim($parts[0]));
4962            $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout'];
4963            $key = trim($parts[1] ?? '');
4964            if (($key != '') || in_array($type, $typesWithOutParameters)) {
4965                switch ($type) {
4966                    case 'gp':
4967                        // Merge GET and POST and get $key out of the merged array
4968                        $getPostArray = GeneralUtility::_GET();
4969                        ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST());
4970                        $retVal = $this->getGlobal($key, $getPostArray);
4971                        break;
4972                    case 'tsfe':
4973                        $retVal = $this->getGlobal('TSFE|' . $key);
4974                        break;
4975                    case 'getenv':
4976                        $retVal = getenv($key);
4977                        break;
4978                    case 'getindpenv':
4979                        $retVal = $this->getEnvironmentVariable($key);
4980                        break;
4981                    case 'field':
4982                        $retVal = $this->getGlobal($key, $fieldArray);
4983                        break;
4984                    case 'file':
4985                        $retVal = $this->getFileDataKey($key);
4986                        break;
4987                    case 'parameters':
4988                        $retVal = $this->parameters[$key];
4989                        break;
4990                    case 'register':
4991                        $retVal = $tsfe->register[$key] ?? null;
4992                        break;
4993                    case 'global':
4994                        $retVal = $this->getGlobal($key);
4995                        break;
4996                    case 'level':
4997                        $retVal = count($tsfe->tmpl->rootLine) - 1;
4998                        break;
4999                    case 'leveltitle':
5000                        $keyParts = GeneralUtility::trimExplode(',', $key);
5001                        $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine);
5002                        $retVal = $this->rootLineValue($numericKey, 'title', strtolower($keyParts[1] ?? '') === 'slide');
5003                        break;
5004                    case 'levelmedia':
5005                        $keyParts = GeneralUtility::trimExplode(',', $key);
5006                        $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine);
5007                        $retVal = $this->rootLineValue($numericKey, 'media', strtolower($keyParts[1] ?? '') === 'slide');
5008                        break;
5009                    case 'leveluid':
5010                        $numericKey = $this->getKey($key, $tsfe->tmpl->rootLine);
5011                        $retVal = $this->rootLineValue($numericKey, 'uid');
5012                        break;
5013                    case 'levelfield':
5014                        $keyParts = GeneralUtility::trimExplode(',', $key);
5015                        $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine);
5016                        $retVal = $this->rootLineValue($numericKey, $keyParts[1], strtolower($keyParts[2] ?? '') === 'slide');
5017                        break;
5018                    case 'fullrootline':
5019                        $keyParts = GeneralUtility::trimExplode(',', $key);
5020                        $fullKey = (int)$keyParts[0] - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine);
5021                        if ($fullKey >= 0) {
5022                            $retVal = $this->rootLineValue($fullKey, $keyParts[1], stristr($keyParts[2] ?? '', 'slide'), $tsfe->rootLine);
5023                        }
5024                        break;
5025                    case 'date':
5026                        if (!$key) {
5027                            $key = 'd/m Y';
5028                        }
5029                        $retVal = date($key, $GLOBALS['EXEC_TIME']);
5030                        break;
5031                    case 'page':
5032                        $retVal = $tsfe->page[$key];
5033                        break;
5034                    case 'pagelayout':
5035                        // Check if the current page has a value in the DB field "backend_layout"
5036                        // if empty, check the root line for "backend_layout_next_level"
5037                        // same as
5038                        //   field = backend_layout
5039                        //   ifEmpty.data = levelfield:-2, backend_layout_next_level, slide
5040                        //   ifEmpty.ifEmpty = default
5041                        $retVal = $tsfe->page['backend_layout'];
5042
5043                        // If it is set to "none" - don't use any
5044                        if ($retVal === '-1') {
5045                            $retVal = 'none';
5046                        } elseif ($retVal === '' || $retVal === '0') {
5047                            // If it not set check the root-line for a layout on next level and use this
5048                            // Remove first element, which is the current page
5049                            // See also \TYPO3\CMS\Backend\View\BackendLayoutView::getSelectedCombinedIdentifier()
5050                            $rootLine = $tsfe->rootLine;
5051                            array_shift($rootLine);
5052                            foreach ($rootLine as $rootLinePage) {
5053                                $retVal = (string)$rootLinePage['backend_layout_next_level'];
5054                                // If layout for "next level" is set to "none" - don't use any and stop searching
5055                                if ($retVal === '-1') {
5056                                    $retVal = 'none';
5057                                    break;
5058                                }
5059                                if ($retVal !== '' && $retVal !== '0') {
5060                                    // Stop searching if a layout for "next level" is set
5061                                    break;
5062                                }
5063                            }
5064                        }
5065                        if ($retVal === '0' || $retVal === '') {
5066                            $retVal = 'default';
5067                        }
5068                        break;
5069                    case 'current':
5070                        $retVal = $this->data[$this->currentValKey] ?? null;
5071                        break;
5072                    case 'db':
5073                        $selectParts = GeneralUtility::trimExplode(':', $key);
5074                        $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]);
5075                        if (is_array($db_rec) && $selectParts[2]) {
5076                            $retVal = $db_rec[$selectParts[2]];
5077                        }
5078                        break;
5079                    case 'lll':
5080                        $retVal = $tsfe->sL('LLL:' . $key);
5081                        break;
5082                    case 'path':
5083                        try {
5084                            $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key);
5085                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
5086                            // do nothing in case the file path is invalid
5087                            $retVal = null;
5088                        }
5089                        break;
5090                    case 'cobj':
5091                        switch ($key) {
5092                            case 'parentRecordNumber':
5093                                $retVal = $this->parentRecordNumber;
5094                                break;
5095                        }
5096                        break;
5097                    case 'debug':
5098                        switch ($key) {
5099                            case 'rootLine':
5100                                $retVal = DebugUtility::viewArray($tsfe->tmpl->rootLine);
5101                                break;
5102                            case 'fullRootLine':
5103                                $retVal = DebugUtility::viewArray($tsfe->rootLine);
5104                                break;
5105                            case 'data':
5106                                $retVal = DebugUtility::viewArray($this->data);
5107                                break;
5108                            case 'register':
5109                                $retVal = DebugUtility::viewArray($tsfe->register);
5110                                break;
5111                            case 'page':
5112                                $retVal = DebugUtility::viewArray($tsfe->page);
5113                                break;
5114                        }
5115                        break;
5116                    case 'flexform':
5117                        $keyParts = GeneralUtility::trimExplode(':', $key, true);
5118                        if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) {
5119                            $flexFormContent = $this->data[$keyParts[0]];
5120                            if (!empty($flexFormContent)) {
5121                                $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
5122                                $flexFormKey = str_replace('.', '|', $keyParts[1]);
5123                                $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent);
5124                                $retVal = $this->getGlobal($flexFormKey, $settings);
5125                            }
5126                        }
5127                        break;
5128                    case 'session':
5129                        $keyParts = GeneralUtility::trimExplode('|', $key, true);
5130                        $sessionKey = array_shift($keyParts);
5131                        $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey);
5132                        foreach ($keyParts as $keyPart) {
5133                            if (is_object($retVal)) {
5134                                $retVal = $retVal->{$keyPart};
5135                            } elseif (is_array($retVal)) {
5136                                $retVal = $retVal[$keyPart];
5137                            } else {
5138                                $retVal = '';
5139                                break;
5140                            }
5141                        }
5142                        if (!is_scalar($retVal)) {
5143                            $retVal = '';
5144                        }
5145                        break;
5146                    case 'context':
5147                        $context = GeneralUtility::makeInstance(Context::class);
5148                        list($aspectName, $propertyName) = GeneralUtility::trimExplode(':', $key, true, 2);
5149                        $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, '');
5150                        if (is_array($retVal)) {
5151                            $retVal = implode(',', $retVal);
5152                        }
5153                        if (!is_scalar($retVal)) {
5154                            $retVal = '';
5155                        }
5156                        break;
5157                    case 'site':
5158                        $request = $GLOBALS['TYPO3_REQUEST'] ?? null;
5159                        $site = $request ? $request->getAttribute('site') : null;
5160                        if ($site instanceof Site) {
5161                            if ($key === 'identifier') {
5162                                $retVal = $site->getIdentifier();
5163                            } elseif ($key === 'base') {
5164                                $retVal = $site->getBase();
5165                            } else {
5166                                try {
5167                                    $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.');
5168                                } catch (MissingArrayPathException $exception) {
5169                                    $this->logger->warning(sprintf('getData() with "%s" failed', $key), ['exception' => $exception]);
5170                                }
5171                            }
5172                        }
5173                        break;
5174                    case 'sitelanguage':
5175                        $request = $GLOBALS['TYPO3_REQUEST'] ?? null;
5176                        $siteLanguage = $request ? $request->getAttribute('language') : null;
5177                        if ($siteLanguage instanceof SiteLanguage) {
5178                            $config = $siteLanguage->toArray();
5179                            if (isset($config[$key])) {
5180                                $retVal = $config[$key];
5181                            }
5182                        }
5183                        break;
5184                }
5185            }
5186
5187            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) {
5188                $hookObject = GeneralUtility::makeInstance($className);
5189                if (!$hookObject instanceof ContentObjectGetDataHookInterface) {
5190                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480);
5191                }
5192                $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $this);
5193            }
5194        }
5195        return $retVal;
5196    }
5197
5198    /**
5199     * Gets file information. This is a helper function for the getData() method above, which resolves e.g.
5200     * page.10.data = file:current:title
5201     * or
5202     * page.10.data = file:17:title
5203     *
5204     * @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.)
5205     * @return string|int The value as retrieved from the file object.
5206     */
5207    protected function getFileDataKey($key)
5208    {
5209        list($fileUidOrCurrentKeyword, $requestedFileInformationKey) = explode(':', $key, 3);
5210        try {
5211            if ($fileUidOrCurrentKeyword === 'current') {
5212                $fileObject = $this->getCurrentFile();
5213            } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) {
5214                /** @var ResourceFactory $fileFactory */
5215                $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
5216                $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword);
5217            } else {
5218                $fileObject = null;
5219            }
5220        } catch (Exception $exception) {
5221            $this->logger->warning('The file "' . $fileUidOrCurrentKeyword . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]);
5222            $fileObject = null;
5223        }
5224
5225        if ($fileObject instanceof FileInterface) {
5226            // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here:
5227            switch ($requestedFileInformationKey) {
5228                case 'name':
5229                    return $fileObject->getName();
5230                case 'uid':
5231                    if (method_exists($fileObject, 'getUid')) {
5232                        return $fileObject->getUid();
5233                    }
5234                    return 0;
5235                case 'originalUid':
5236                    if ($fileObject instanceof FileReference) {
5237                        return $fileObject->getOriginalFile()->getUid();
5238                    }
5239                    return null;
5240                case 'size':
5241                    return $fileObject->getSize();
5242                case 'sha1':
5243                    return $fileObject->getSha1();
5244                case 'extension':
5245                    return $fileObject->getExtension();
5246                case 'mimetype':
5247                    return $fileObject->getMimeType();
5248                case 'contents':
5249                    return $fileObject->getContents();
5250                case 'publicUrl':
5251                    return $fileObject->getPublicUrl();
5252                default:
5253                    // Generic alternative here
5254                    return $fileObject->getProperty($requestedFileInformationKey);
5255            }
5256        } else {
5257            // @todo fail silently as is common in tslib_content
5258            return 'Error: no file object';
5259        }
5260    }
5261
5262    /**
5263     * Returns a value from the current rootline (site) from $GLOBALS['TSFE']->tmpl->rootLine;
5264     *
5265     * @param string $key Which level in the root line
5266     * @param string $field The field in the rootline record to return (a field from the pages table)
5267     * @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
5268     * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array
5269     * @return string The value from the field of the rootline.
5270     * @internal
5271     * @see getData()
5272     */
5273    public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '')
5274    {
5275        $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine;
5276        if (!$slideBack) {
5277            return $rootLine[$key][$field];
5278        }
5279        for ($a = $key; $a >= 0; $a--) {
5280            $val = $rootLine[$a][$field];
5281            if ($val) {
5282                return $val;
5283            }
5284        }
5285
5286        return '';
5287    }
5288
5289    /**
5290     * Return global variable where the input string $var defines array keys separated by "|"
5291     * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
5292     *
5293     * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
5294     * @param array $source Alternative array than $GLOBAL to get variables from.
5295     * @return mixed Whatever value. If none, then blank string.
5296     * @see getData()
5297     */
5298    public function getGlobal($keyString, $source = null)
5299    {
5300        $keys = explode('|', $keyString);
5301        $numberOfLevels = count($keys);
5302        $rootKey = trim($keys[0]);
5303        $value = isset($source) ? $source[$rootKey] : $GLOBALS[$rootKey];
5304        for ($i = 1; $i < $numberOfLevels && isset($value); $i++) {
5305            $currentKey = trim($keys[$i]);
5306            if (is_object($value)) {
5307                $value = $value->{$currentKey};
5308            } elseif (is_array($value)) {
5309                $value = $value[$currentKey];
5310            } else {
5311                $value = '';
5312                break;
5313            }
5314        }
5315        if (!is_scalar($value)) {
5316            $value = '';
5317        }
5318        return $value;
5319    }
5320
5321    /**
5322     * 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).
5323     * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost...
5324     *
5325     * @param int $key The integer to transform
5326     * @param array $arr array in which the key should be found.
5327     * @return int The processed integer key value.
5328     * @internal
5329     * @see getData()
5330     */
5331    public function getKey($key, $arr)
5332    {
5333        $key = (int)$key;
5334        if (is_array($arr)) {
5335            if ($key < 0) {
5336                $key = count($arr) + $key;
5337            }
5338            if ($key < 0) {
5339                $key = 0;
5340            }
5341        }
5342        return $key;
5343    }
5344
5345    /***********************************************
5346     *
5347     * Link functions (typolink)
5348     *
5349     ***********************************************/
5350
5351    /**
5352     * called from the typoLink() function
5353     *
5354     * does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1"
5355     * into separate parts
5356     *
5357     * @param string $linkText The string (text) to link
5358     * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
5359     * @param array $configuration TypoScript configuration
5360     * @return array|string
5361     * @see typoLink()
5362     *
5363     * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links
5364     */
5365    protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
5366    {
5367        $linkParameter = null;
5368
5369        // Link parameter value = first part
5370        $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
5371
5372        // Check for link-handler keyword
5373        $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2);
5374        $linkHandlerKeyword = $linkHandlerExploded[0] ?? null;
5375        $linkHandlerValue = $linkHandlerExploded[1] ?? null;
5376        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword])
5377            && (string)$linkHandlerValue !== ''
5378        ) {
5379            $linkHandlerObj = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword]);
5380            if (method_exists($linkHandlerObj, 'main')) {
5381                return $linkHandlerObj->main($linkText, $configuration, $linkHandlerKeyword, $linkHandlerValue, $mixedLinkParameter, $this);
5382            }
5383        }
5384
5385        if (in_array(strtolower(preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
5386            // Disallow insecure scheme's like javascript: or data:
5387            return $linkText;
5388        }
5389        $linkParameter = $linkParameterParts['url'];
5390
5391        // additional parameters that need to be set
5392        if ($linkParameterParts['additionalParams'] !== '') {
5393            $forceParams = $linkParameterParts['additionalParams'];
5394            // params value
5395            $configuration['additionalParams'] .= $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
5396        }
5397
5398        return [
5399            'href'   => $linkParameter,
5400            'target' => $linkParameterParts['target'],
5401            'class'  => $linkParameterParts['class'],
5402            'title'  => $linkParameterParts['title']
5403        ];
5404    }
5405
5406    /**
5407     * Implements the "typolink" property of stdWrap (and others)
5408     * 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.
5409     * 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.
5410     * 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.
5411     * For many more details on the parameters and how they are interpreted, please see the link to TSref below.
5412     *
5413     * the FAL API is handled with the namespace/prefix "file:..."
5414     *
5415     * @param string $linkText The string (text) to link
5416     * @param array $conf TypoScript configuration (see link below)
5417     * @return string A link-wrapped string.
5418     * @see stdWrap(), \TYPO3\CMS\Frontend\Plugin\AbstractPlugin::pi_linkTP()
5419     */
5420    public function typoLink($linkText, $conf)
5421    {
5422        $linkText = (string)$linkText;
5423        $tsfe = $this->getTypoScriptFrontendController();
5424
5425        $linkParameter = trim(
5426            (isset($conf['parameter.']) ?? '')
5427            ? $this->stdWrap($conf['parameter'] ?? '', $conf['parameter.'])
5428            : ($conf['parameter'] ?? '')
5429        );
5430        $this->lastTypoLinkUrl = '';
5431        $this->lastTypoLinkTarget = '';
5432
5433        $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf);
5434        // check if the link handler hook has resolved the link completely already
5435        if (!is_array($resolvedLinkParameters)) {
5436            return $resolvedLinkParameters;
5437        }
5438        $linkParameter = $resolvedLinkParameters['href'];
5439        $target = $resolvedLinkParameters['target'];
5440        $title = $resolvedLinkParameters['title'];
5441
5442        if (!$linkParameter) {
5443            return $this->resolveAnchorLink($linkText, $conf ?? []);
5444        }
5445
5446        // Detecting kind of link and resolve all necessary parameters
5447        $linkService = GeneralUtility::makeInstance(LinkService::class);
5448        try {
5449            $linkDetails = $linkService->resolve($linkParameter);
5450        } catch (Exception\InvalidPathException $exception) {
5451            $this->logger->warning('The link could not be generated', ['exception' => $exception]);
5452            return $linkText;
5453        }
5454
5455        $linkDetails['typoLinkParameter'] = $linkParameter;
5456        if (isset($linkDetails['type']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
5457            /** @var AbstractTypolinkBuilder $linkBuilder */
5458            $linkBuilder = GeneralUtility::makeInstance(
5459                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
5460                $this,
5461                $tsfe
5462            );
5463            try {
5464                list($this->lastTypoLinkUrl, $linkText, $target) = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
5465                $this->lastTypoLinkTarget = htmlspecialchars($target);
5466                $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
5467                $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
5468            } catch (UnableToLinkException $e) {
5469                $this->logger->debug(sprintf('Unable to link "%s": %s', $e->getLinkText(), $e->getMessage()), ['exception' => $e]);
5470
5471                // Only return the link text directly
5472                return $e->getLinkText();
5473            }
5474        } elseif (isset($linkDetails['url'])) {
5475            $this->lastTypoLinkUrl = $linkDetails['url'];
5476            $this->lastTypoLinkTarget = htmlspecialchars($target);
5477            $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
5478            $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
5479        } else {
5480            return $linkText;
5481        }
5482
5483        // We need to backup the URL because ATagParams might call typolink again and change the last URL.
5484        $url = $this->lastTypoLinkUrl;
5485        $finalTagParts = [
5486            'aTagParams' => $this->getATagParams($conf) . $this->extLinkATagParams($this->lastTypoLinkUrl, $linkDetails['type']),
5487            'url'        => $url,
5488            'TYPE'       => $linkDetails['type']
5489        ];
5490
5491        // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
5492        if (!empty($finalTagParts['aTagParams'])) {
5493            $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'], true);
5494            if (isset($aTagParams['href'])) {
5495                unset($aTagParams['href']);
5496                $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams, true);
5497            }
5498        }
5499
5500        // Building the final <a href=".."> tag
5501        $tagAttributes = [];
5502
5503        // Title attribute
5504        if (empty($title)) {
5505            $title = $conf['title'] ?? '';
5506            if (isset($conf['title.']) && is_array($conf['title.'])) {
5507                $title = $this->stdWrap($title, $conf['title.']);
5508            }
5509        }
5510
5511        // Check, if the target is coded as a JS open window link:
5512        $JSwindowParts = [];
5513        $JSwindowParams = '';
5514        if ($target && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $target, $JSwindowParts)) {
5515            // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
5516            $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower($conf['JSwindow_params'] . ',' . $JSwindowParts[4]), true);
5517            $JSwindow_paramsArr = [];
5518            foreach ($JSwindow_tempParamsArr as $JSv) {
5519                list($JSp, $JSv) = explode('=', $JSv, 2);
5520                $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
5521            }
5522            // Add width/height:
5523            $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
5524            $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
5525            // Imploding into string:
5526            $JSwindowParams = implode(',', $JSwindow_paramsArr);
5527        }
5528
5529        if (!$JSwindowParams && $linkDetails['type'] === LinkService::TYPE_EMAIL && $tsfe->spamProtectEmailAddresses === 'ascii') {
5530            $tagAttributes['href'] = $finalTagParts['url'];
5531        } else {
5532            $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']);
5533        }
5534        if (!empty($title)) {
5535            $tagAttributes['title'] = htmlspecialchars($title);
5536        }
5537
5538        // Target attribute
5539        if (!empty($target)) {
5540            $tagAttributes['target'] = htmlspecialchars($target);
5541        } elseif ($JSwindowParams && !in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
5542            // Create TARGET-attribute only if the right doctype is used
5543            $tagAttributes['target'] = 'FEopenLink';
5544        }
5545
5546        if ($JSwindowParams) {
5547            $onClick = 'openPic(' . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($finalTagParts['url'])) . ',\'FEopenLink\',' . GeneralUtility::quoteJSvalue($JSwindowParams) . ');return false;';
5548            $tagAttributes['onclick'] = htmlspecialchars($onClick);
5549            $this->getTypoScriptFrontendController()->setJS('openPic');
5550        }
5551
5552        if (!empty($resolvedLinkParameters['class'])) {
5553            $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']);
5554        }
5555
5556        // Prevent trouble with double and missing spaces between attributes and merge params before implode
5557        // (skip decoding HTML entities, since `$tagAttributes` are expected to be encoded already)
5558        $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
5559        $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
5560
5561        // kept for backwards-compatibility in hooks
5562        $finalTagParts['targetParams'] = !empty($tagAttributes['target']) ? ' target="' . $tagAttributes['target'] . '"' : '';
5563        $this->lastTypoLinkTarget = $target;
5564
5565        // Call user function:
5566        if ($conf['userFunc'] ?? false) {
5567            $finalTagParts['TAG'] = $finalAnchorTag;
5568            $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $finalTagParts);
5569        }
5570
5571        // Hook: Call post processing function for link rendering:
5572        $_params = [
5573            'conf' => &$conf,
5574            'linktxt' => &$linkText,
5575            'finalTag' => &$finalAnchorTag,
5576            'finalTagParts' => &$finalTagParts,
5577            'linkDetails' => &$linkDetails,
5578            'tagAttributes' => &$finalTagAttributes
5579        ];
5580        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) {
5581            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
5582        }
5583
5584        // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made:
5585        if ($conf['returnLast'] ?? false) {
5586            switch ($conf['returnLast']) {
5587                case 'url':
5588                    return $this->lastTypoLinkUrl;
5589                case 'target':
5590                    return $this->lastTypoLinkTarget;
5591            }
5592        }
5593
5594        $wrap = isset($conf['wrap.'])
5595            ? $this->stdWrap($conf['wrap'] ?? '', $conf['wrap.'])
5596            : $conf['wrap'] ?? '';
5597
5598        if ($conf['ATagBeforeWrap'] ?? false) {
5599            return $finalAnchorTag . $this->wrap($linkText, $wrap) . '</a>';
5600        }
5601        return $this->wrap($finalAnchorTag . $linkText . '</a>', $wrap);
5602    }
5603
5604    /**
5605     * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
5606     *
5607     * @param array $conf TypoScript properties for "typolink
5608     * @return string The URL of the link-tag that typolink() would by itself return
5609     * @see typoLink()
5610     */
5611    public function typoLink_URL($conf)
5612    {
5613        $this->typoLink('|', $conf);
5614        return $this->lastTypoLinkUrl;
5615    }
5616
5617    /**
5618     * Returns a linked string made from typoLink parameters.
5619     *
5620     * 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.
5621     * Optionally you can supply $urlParameters which is an array with key/value pairs that are rawurlencoded and appended to the resulting url.
5622     *
5623     * @param string $label Text string being wrapped by the link.
5624     * @param string $params Link parameter; eg. "123" for page id, "kasperYYYY@typo3.com" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
5625     * @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.
5626     * @param string $target Specific target set, if any. (Default is using the current)
5627     * @return string The wrapped $label-text string
5628     * @see getTypoLink_URL()
5629     */
5630    public function getTypoLink($label, $params, $urlParameters = [], $target = '')
5631    {
5632        $conf = [];
5633        $conf['parameter'] = $params;
5634        if ($target) {
5635            $conf['target'] = $target;
5636            $conf['extTarget'] = $target;
5637            $conf['fileTarget'] = $target;
5638        }
5639        if (is_array($urlParameters)) {
5640            if (!empty($urlParameters)) {
5641                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
5642            }
5643        } else {
5644            $conf['additionalParams'] .= $urlParameters;
5645        }
5646        $out = $this->typoLink($label, $conf);
5647        return $out;
5648    }
5649
5650    /**
5651     * Returns the canonical URL to the current "location", which include the current page ID and type
5652     * and optionally the query string
5653     *
5654     * @param bool $addQueryString Whether additional GET arguments in the query string should be included or not
5655     * @return string
5656     */
5657    public function getUrlToCurrentLocation($addQueryString = true)
5658    {
5659        $conf = [];
5660        $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type;
5661        if ($addQueryString) {
5662            $conf['addQueryString'] = '1';
5663            $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars)));
5664            $conf['addQueryString.'] = [
5665                'method' => 'GET',
5666                'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : '')
5667            ];
5668            $conf['useCacheHash'] = GeneralUtility::_GET('cHash') ? '1' : '0';
5669        }
5670
5671        return $this->typoLink_URL($conf);
5672    }
5673
5674    /**
5675     * Returns the URL of a "typolink" create from the input parameter string, url-parameters and target
5676     *
5677     * @param string $params Link parameter; eg. "123" for page id, "kasperYYYY@typo3.com" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
5678     * @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.
5679     * @param string $target Specific target set, if any. (Default is using the current)
5680     * @return string The URL
5681     * @see getTypoLink()
5682     */
5683    public function getTypoLink_URL($params, $urlParameters = [], $target = '')
5684    {
5685        $this->getTypoLink('', $params, $urlParameters, $target);
5686        return $this->lastTypoLinkUrl;
5687    }
5688
5689    /**
5690     * Generates a typolink and returns the two link tags - start and stop - in an array
5691     *
5692     * @param array $conf "typolink" TypoScript properties
5693     * @return array An array with two values in key 0+1, each value being the start and close <a>-tag of the typolink properties being inputted in $conf
5694     * @see typolink()
5695     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use typoLink() instead.
5696     */
5697    public function typolinkWrap($conf)
5698    {
5699        trigger_error('ContentObjectRenderer->typolinkWrap() will be removed in TYPO3 v10.0. Use $cObj->typoLink() instead.', E_USER_DEPRECATED);
5700        $k = md5(microtime());
5701        return explode($k, $this->typoLink($k, $conf));
5702    }
5703
5704    /**
5705     * Returns the current page URL
5706     *
5707     * @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.
5708     * @param int $id An alternative ID to the current id ($GLOBALS['TSFE']->id)
5709     * @return string The URL
5710     * @see getTypoLink_URL()
5711     * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use getTypoLink_URL() instead.
5712     */
5713    public function currentPageUrl($urlParameters = [], $id = 0)
5714    {
5715        trigger_error('ContentObjectRenderer->currentPageUrl() will be removed in TYPO3 v10.0. Use $cObj->getTypoLink_URL() instead.', E_USER_DEPRECATED);
5716        $tsfe = $this->getTypoScriptFrontendController();
5717        return $this->getTypoLink_URL($id ?: $tsfe->id, $urlParameters, $tsfe->sPre);
5718    }
5719
5720    /**
5721     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
5722     *
5723     * @param string $context The context in which the method is called (e.g. typoLink).
5724     * @param string $url The URL that should be processed.
5725     * @param array $typolinkConfiguration The current link configuration array.
5726     * @return string|null Returns NULL if URL was not processed or the processed URL as a string.
5727     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
5728     */
5729    protected function processUrl($context, $url, $typolinkConfiguration = [])
5730    {
5731        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
5732        if (empty($urlProcessors)) {
5733            return $url;
5734        }
5735
5736        foreach ($urlProcessors as $identifier => $configuration) {
5737            if (empty($configuration) || !is_array($configuration)) {
5738                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
5739            }
5740            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
5741                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
5742            }
5743        }
5744
5745        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
5746        $keepProcessing = true;
5747
5748        foreach ($orderedProcessors as $configuration) {
5749            /** @var UrlProcessorInterface $urlProcessor */
5750            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
5751            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
5752            if (!$keepProcessing) {
5753                break;
5754            }
5755        }
5756
5757        return $url;
5758    }
5759
5760    /**
5761     * Creates a href attibute for given $mailAddress.
5762     * The function uses spamProtectEmailAddresses for encoding the mailto statement.
5763     * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:user@example.tld".
5764     *
5765     * @param string $mailAddress Email address
5766     * @param string $linktxt Link text, default will be the email address.
5767     * @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.
5768     */
5769    public function getMailTo($mailAddress, $linktxt)
5770    {
5771        $mailAddress = (string)$mailAddress;
5772        if ((string)$linktxt === '') {
5773            $linktxt = htmlspecialchars($mailAddress);
5774        }
5775
5776        $originalMailToUrl = 'mailto:' . $mailAddress;
5777        $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
5778
5779        // no processing happened, therefore, the default processing kicks in
5780        if ($mailToUrl === $originalMailToUrl) {
5781            $tsfe = $this->getTypoScriptFrontendController();
5782            if ($tsfe->spamProtectEmailAddresses) {
5783                $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
5784                if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
5785                    $encodedForJsAndHref = rawurlencode(GeneralUtility::quoteJSvalue($mailToUrl));
5786                    $mailToUrl = 'javascript:linkTo_UnCryptMailto(' . $encodedForJsAndHref . ');';
5787                }
5788                $atLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst']) ?: '(at)';
5789                $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
5790                if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']) {
5791                    $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
5792                    $lastDotLabel = $lastDotLabel ? $lastDotLabel : '(dot)';
5793                    $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
5794                }
5795                $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
5796            }
5797        }
5798
5799        return [$mailToUrl, $linktxt];
5800    }
5801
5802    /**
5803     * Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
5804     *
5805     * @param string $string Input string to en/decode: "mailto:some@example.com
5806     * @param mixed  $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses
5807     * @return string encoded version of $string
5808     */
5809    protected function encryptEmail(string $string, $type): string
5810    {
5811        $out = '';
5812        // obfuscates using the decimal HTML entity references for each character
5813        if ($type === 'ascii') {
5814            foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5815                $out .= '&#' . mb_ord($char) . ';';
5816            }
5817        } else {
5818            // like str_rot13() but with a variable offset and a wider character range
5819            $len = strlen($string);
5820            $offset = (int)$type;
5821            for ($i = 0; $i < $len; $i++) {
5822                $charValue = ord($string[$i]);
5823                // 0-9 . , - + / :
5824                if ($charValue >= 43 && $charValue <= 58) {
5825                    $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5826                } elseif ($charValue >= 64 && $charValue <= 90) {
5827                    // A-Z @
5828                    $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5829                } elseif ($charValue >= 97 && $charValue <= 122) {
5830                    // a-z
5831                    $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5832                } else {
5833                    $out .= $string[$i];
5834                }
5835            }
5836        }
5837        return $out;
5838    }
5839
5840    /**
5841     * Decryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
5842     *
5843     * @param string $string Input string to en/decode: "mailto:some@example.com
5844     * @param mixed  $type - either "ascii" or a number between -10 and 10 taken from config.spamProtectEmailAddresses
5845     * @return string decoded version of $string
5846     */
5847    protected function decryptEmail(string $string, $type): string
5848    {
5849        $out = '';
5850        // obfuscates using the decimal HTML entity references for each character
5851        if ($type === 'ascii') {
5852            foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5853                $out .= '&#' . mb_ord($char) . ';';
5854            }
5855        } else {
5856            // like str_rot13() but with a variable offset and a wider character range
5857            $len = strlen($string);
5858            $offset = (int)$type * -1;
5859            for ($i = 0; $i < $len; $i++) {
5860                $charValue = ord($string[$i]);
5861                // 0-9 . , - + / :
5862                if ($charValue >= 43 && $charValue <= 58) {
5863                    $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5864                } elseif ($charValue >= 64 && $charValue <= 90) {
5865                    // A-Z @
5866                    $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5867                } elseif ($charValue >= 97 && $charValue <= 122) {
5868                    // a-z
5869                    $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5870                } else {
5871                    $out .= $string[$i];
5872                }
5873            }
5874        }
5875        return $out;
5876    }
5877
5878    /**
5879     * Encryption (or decryption) of a single character.
5880     * Within the given range the character is shifted with the supplied offset.
5881     *
5882     * @param int $n Ordinal of input character
5883     * @param int $start Start of range
5884     * @param int $end End of range
5885     * @param int $offset Offset
5886     * @return string encoded/decoded version of character
5887     */
5888    protected function encryptCharcode($n, $start, $end, $offset)
5889    {
5890        $n = $n + $offset;
5891        if ($offset > 0 && $n > $end) {
5892            $n = $start + ($n - $end - 1);
5893        } elseif ($offset < 0 && $n < $start) {
5894            $n = $end - ($start - $n - 1);
5895        }
5896        return chr($n);
5897    }
5898
5899    /**
5900     * Gets the query arguments and assembles them for URLs.
5901     * Arguments may be removed or set, depending on configuration.
5902     *
5903     * @param array $conf Configuration
5904     * @param array $overruleQueryArguments Multidimensional key/value pairs that overrule incoming query arguments
5905     * @param bool $forceOverruleArguments If set, key/value pairs not in the query but the overrule array will be set
5906     * @return string The URL query part (starting with a &)
5907     */
5908    public function getQueryArguments($conf, $overruleQueryArguments = [], $forceOverruleArguments = false)
5909    {
5910        $method = (string)($conf['method'] ?? '');
5911        switch ($method) {
5912            case 'GET':
5913                $currentQueryArray = GeneralUtility::_GET();
5914                break;
5915            case 'POST':
5916                $currentQueryArray = GeneralUtility::_POST();
5917                break;
5918            case 'GET,POST':
5919                $currentQueryArray = GeneralUtility::_GET();
5920                ArrayUtility::mergeRecursiveWithOverrule($currentQueryArray, GeneralUtility::_POST());
5921                break;
5922            case 'POST,GET':
5923                $currentQueryArray = GeneralUtility::_POST();
5924                ArrayUtility::mergeRecursiveWithOverrule($currentQueryArray, GeneralUtility::_GET());
5925                break;
5926            default:
5927                $currentQueryArray = [];
5928                parse_str($this->getEnvironmentVariable('QUERY_STRING'), $currentQueryArray);
5929        }
5930        if ($conf['exclude'] ?? false) {
5931            $excludeString = str_replace(',', '&', $conf['exclude']);
5932            $excludedQueryParts = [];
5933            parse_str($excludeString, $excludedQueryParts);
5934            // never repeat id
5935            $exclude['id'] = 0;
5936            $newQueryArray = ArrayUtility::arrayDiffAssocRecursive($currentQueryArray, $excludedQueryParts);
5937        } else {
5938            $newQueryArray = $currentQueryArray;
5939        }
5940        ArrayUtility::mergeRecursiveWithOverrule($newQueryArray, $overruleQueryArguments, $forceOverruleArguments);
5941        return HttpUtility::buildQueryString($newQueryArray, '&');
5942    }
5943
5944    /***********************************************
5945     *
5946     * Miscellaneous functions, stand alone
5947     *
5948     ***********************************************/
5949    /**
5950     * Wrapping a string.
5951     * Implements the TypoScript "wrap" property.
5952     * Example: $content = "HELLO WORLD" and $wrap = "<strong> | </strong>", result: "<strong>HELLO WORLD</strong>"
5953     *
5954     * @param string $content The content to wrap
5955     * @param string $wrap The wrap value, eg. "<strong> | </strong>
5956     * @param string $char The char used to split the wrapping value, default is "|
5957     * @return string Wrapped input string
5958     * @see noTrimWrap()
5959     */
5960    public function wrap($content, $wrap, $char = '|')
5961    {
5962        if ($wrap) {
5963            $wrapArr = explode($char, $wrap);
5964            $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
5965        }
5966        return $content;
5967    }
5968
5969    /**
5970     * Wrapping a string, preserving whitespace in wrap value.
5971     * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does)
5972     *
5973     * @param string $content The content to wrap, eg. "HELLO WORLD
5974     * @param string $wrap The wrap value, eg. " | <strong> | </strong>
5975     * @param string $char The char used to split the wrapping value, default is "|"
5976     * @return string Wrapped input string, eg. " <strong> HELLO WORD </strong>
5977     * @see wrap()
5978     */
5979    public function noTrimWrap($content, $wrap, $char = '|')
5980    {
5981        if ($wrap) {
5982            // expects to be wrapped with (at least) 3 characters (before, middle, after)
5983            // anything else is not taken into account
5984            $wrapArr = explode($char, $wrap, 4);
5985            $content = $wrapArr[1] . $content . $wrapArr[2];
5986        }
5987        return $content;
5988    }
5989
5990    /**
5991     * Calling a user function/class-method
5992     * Notice: For classes the instantiated object will have the internal variable, $cObj, set to be a *reference* to $this (the parent/calling object).
5993     *
5994     * @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.
5995     * @param array $conf The TypoScript configuration to pass the function
5996     * @param string $content The content string to pass the function
5997     * @return string The return content from the function call. Should probably be a string.
5998     * @see USER(), stdWrap(), typoLink(), _parseFunc()
5999     */
6000    public function callUserFunction($funcName, $conf, $content)
6001    {
6002        // Split parts
6003        $parts = explode('->', $funcName);
6004        if (count($parts) === 2) {
6005            // Check whether PHP class is available
6006            if (class_exists($parts[0])) {
6007                $classObj = GeneralUtility::makeInstance($parts[0]);
6008                if (is_object($classObj) && method_exists($classObj, $parts[1])) {
6009                    $classObj->cObj = $this;
6010                    $content = call_user_func_array([
6011                        $classObj,
6012                        $parts[1]
6013                    ], [
6014                        $content,
6015                        $conf
6016                    ]);
6017                } else {
6018                    $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', 3);
6019                }
6020            } else {
6021                $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', 3);
6022            }
6023        } elseif (function_exists($funcName)) {
6024            $content = call_user_func($funcName, $content, $conf);
6025        } else {
6026            $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', 3);
6027        }
6028        return $content;
6029    }
6030
6031    /**
6032     * Cleans up a string of keywords. Keywords at splitted by "," (comma)  ";" (semi colon) and linebreak
6033     *
6034     * @param string $content String of keywords
6035     * @return string Cleaned up string, keywords will be separated by a comma only.
6036     */
6037    public function keywords($content)
6038    {
6039        $listArr = preg_split('/[,;' . LF . ']/', $content);
6040        foreach ($listArr as $k => $v) {
6041            $listArr[$k] = trim($v);
6042        }
6043        return implode(',', $listArr);
6044    }
6045
6046    /**
6047     * Changing character case of a string, converting typically used western charset characters as well.
6048     *
6049     * @param string $theValue The string to change case for.
6050     * @param string $case The direction; either "upper" or "lower
6051     * @return string
6052     * @see HTMLcaseshift()
6053     */
6054    public function caseshift($theValue, $case)
6055    {
6056        switch (strtolower($case)) {
6057            case 'upper':
6058                $theValue = mb_strtoupper($theValue, 'utf-8');
6059                break;
6060            case 'lower':
6061                $theValue = mb_strtolower($theValue, 'utf-8');
6062                break;
6063            case 'capitalize':
6064                $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8');
6065                break;
6066            case 'ucfirst':
6067                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
6068                $firstChar = mb_strtoupper($firstChar, 'utf-8');
6069                $remainder = mb_substr($theValue, 1, null, 'utf-8');
6070                $theValue = $firstChar . $remainder;
6071                break;
6072            case 'lcfirst':
6073                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
6074                $firstChar = mb_strtolower($firstChar, 'utf-8');
6075                $remainder = mb_substr($theValue, 1, null, 'utf-8');
6076                $theValue = $firstChar . $remainder;
6077                break;
6078            case 'uppercamelcase':
6079                $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue);
6080                break;
6081            case 'lowercamelcase':
6082                $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue);
6083                break;
6084        }
6085        return $theValue;
6086    }
6087
6088    /**
6089     * Shifts the case of characters outside of HTML tags in the input string
6090     *
6091     * @param string $theValue The string to change case for.
6092     * @param string $case The direction; either "upper" or "lower
6093     * @return string
6094     * @see caseshift()
6095     */
6096    public function HTMLcaseshift($theValue, $case)
6097    {
6098        $inside = 0;
6099        $newVal = '';
6100        $pointer = 0;
6101        $totalLen = strlen($theValue);
6102        do {
6103            if (!$inside) {
6104                $len = strcspn(substr($theValue, $pointer), '<');
6105                $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case);
6106                $inside = 1;
6107            } else {
6108                $len = strcspn(substr($theValue, $pointer), '>') + 1;
6109                $newVal .= substr($theValue, $pointer, $len);
6110                $inside = 0;
6111            }
6112            $pointer += $len;
6113        } while ($pointer < $totalLen);
6114        return $newVal;
6115    }
6116
6117    /**
6118     * Returns the 'age' of the tstamp $seconds
6119     *
6120     * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs
6121     * @param string $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs'
6122     * @return string The formatted string
6123     */
6124    public function calcAge($seconds, $labels)
6125    {
6126        if (MathUtility::canBeInterpretedAsInteger($labels)) {
6127            $labels = ' min| hrs| days| yrs| min| hour| day| year';
6128        } else {
6129            $labels = str_replace('"', '', $labels);
6130        }
6131        $labelArr = explode('|', $labels);
6132        if (count($labelArr) === 4) {
6133            $labelArr = array_merge($labelArr, $labelArr);
6134        }
6135        $absSeconds = abs($seconds);
6136        $sign = $seconds > 0 ? 1 : -1;
6137        if ($absSeconds < 3600) {
6138            $val = round($absSeconds / 60);
6139            $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]);
6140        } elseif ($absSeconds < 24 * 3600) {
6141            $val = round($absSeconds / 3600);
6142            $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]);
6143        } elseif ($absSeconds < 365 * 24 * 3600) {
6144            $val = round($absSeconds / (24 * 3600));
6145            $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]);
6146        } else {
6147            $val = round($absSeconds / (365 * 24 * 3600));
6148            $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null));
6149        }
6150        return $seconds;
6151    }
6152
6153    /**
6154     * Sends a notification email
6155     *
6156     * @param string $message The message content. If blank, no email is sent.
6157     * @param string $recipients Comma list of recipient email addresses
6158     * @param string $cc Email address of recipient of an extra mail. The same mail will be sent ONCE more; not using a CC header but sending twice.
6159     * @param string $senderAddress "From" email address
6160     * @param string $senderName Optional "From" name
6161     * @param string $replyTo Optional "Reply-To" header email address.
6162     * @return bool Returns TRUE if sent
6163     */
6164    public function sendNotifyEmail($message, $recipients, $cc, $senderAddress, $senderName = '', $replyTo = '')
6165    {
6166        /** @var MailMessage $mail */
6167        $mail = GeneralUtility::makeInstance(MailMessage::class);
6168        $senderName = trim($senderName);
6169        $senderAddress = trim($senderAddress);
6170        if ($senderName !== '' && $senderAddress !== '') {
6171            $mail->setFrom([$senderAddress => $senderName]);
6172        } elseif ($senderAddress !== '') {
6173            $mail->setFrom([$senderAddress]);
6174        }
6175        $parsedReplyTo = MailUtility::parseAddresses($replyTo);
6176        if (!empty($parsedReplyTo)) {
6177            $mail->setReplyTo($parsedReplyTo);
6178        }
6179        $message = trim($message);
6180        if ($message !== '') {
6181            // First line is subject
6182            $messageParts = explode(LF, $message, 2);
6183            $subject = trim($messageParts[0]);
6184            $plainMessage = trim($messageParts[1]);
6185            $parsedRecipients = MailUtility::parseAddresses($recipients);
6186            if (!empty($parsedRecipients)) {
6187                $mail->setTo($parsedRecipients)
6188                    ->setSubject($subject)
6189                    ->setBody($plainMessage);
6190                $mail->send();
6191            }
6192            $parsedCc = MailUtility::parseAddresses($cc);
6193            if (!empty($parsedCc)) {
6194                $from = $mail->getFrom();
6195                /** @var MailMessage $mail */
6196                $mail = GeneralUtility::makeInstance(MailMessage::class);
6197                if (!empty($parsedReplyTo)) {
6198                    $mail->setReplyTo($parsedReplyTo);
6199                }
6200                $mail->setFrom($from)
6201                    ->setTo($parsedCc)
6202                    ->setSubject($subject)
6203                    ->setBody($plainMessage);
6204                $mail->send();
6205            }
6206            return true;
6207        }
6208        return false;
6209    }
6210
6211    /**
6212     * Resolves a TypoScript reference value to the full set of properties BUT overridden with any local properties set.
6213     * So the reference is resolved but overlaid with local TypoScript properties of the reference value.
6214     *
6215     * @param array $confArr The TypoScript array
6216     * @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.
6217     * @return array The modified TypoScript array
6218     */
6219    public function mergeTSRef($confArr, $prop)
6220    {
6221        if ($confArr[$prop][0] === '<') {
6222            $key = trim(substr($confArr[$prop], 1));
6223            $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
6224            // $name and $conf is loaded with the referenced values.
6225            $old_conf = $confArr[$prop . '.'];
6226            list(, $conf) = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
6227            if (is_array($old_conf) && !empty($old_conf)) {
6228                $conf = is_array($conf) ? array_replace_recursive($conf, $old_conf) : $old_conf;
6229            }
6230            $confArr[$prop . '.'] = $conf;
6231        }
6232        return $confArr;
6233    }
6234
6235    /***********************************************
6236     *
6237     * Database functions, making of queries
6238     *
6239     ***********************************************/
6240
6241    /**
6242     * Returns a part of a WHERE clause which will filter out records with start/end times or hidden/fe_groups fields
6243     * set to values that should de-select them according to the current time, preview settings or user login.
6244     * Definitely a frontend function.
6245     * THIS IS A VERY IMPORTANT FUNCTION: Basically you must add the output from this function for EVERY select query you create
6246     * for selecting records of tables in your own applications - thus they will always be filtered according to the "enablefields"
6247     * configured in TCA
6248     * Simply calls \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() BUT will send the show_hidden flag along!
6249     * This means this function will work in conjunction with the preview facilities of the frontend engine/Admin Panel.
6250     *
6251     * @param string $table The table for which to get the where clause
6252     * @param bool $show_hidden If set, then you want NOT to filter out hidden records. Otherwise hidden record are filtered based on the current preview settings.
6253     * @param array $ignore_array Array you can pass where keys can be "disabled", "starttime", "endtime", "fe_group" (keys from "enablefields" in TCA) and if set they will make sure that part of the clause is not added. Thus disables the specific part of the clause. For previewing etc.
6254     * @return string The part of the where clause on the form " AND [fieldname]=0 AND ...". Eg. " AND hidden=0 AND starttime < 123345567
6255     * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
6256     */
6257    public function enableFields($table, $show_hidden = false, array $ignore_array = [])
6258    {
6259        trigger_error('cObj->enableFields() will be removed in TYPO3 v10.0. should be used from the PageRepository->enableFields() functionality directly.', E_USER_DEPRECATED);
6260        return $this->getTypoScriptFrontendController()->sys_page->enableFields($table, $show_hidden ? true : -1, $ignore_array);
6261    }
6262
6263    /**
6264     * Generates a list of Page-uid's from $id. List does not include $id itself
6265     * (unless the id specified is negative in which case it does!)
6266     * The only pages WHICH PREVENTS DECENDING in a branch are
6267     * - deleted pages,
6268     * - pages in a recycler (doktype = 255) or of the Backend User Section (doktpe = 6) type
6269     * - pages that has the extendToSubpages set, WHERE start/endtime, hidden
6270     * and fe_users would hide the records.
6271     * Apart from that, pages with enable-fields excluding them, will also be
6272     * removed. HOWEVER $dontCheckEnableFields set will allow
6273     * enableFields-excluded pages to be included anyway - including
6274     * extendToSubpages sections!
6275     * Mount Pages are also descended but notice that these ID numbers are not
6276     * useful for links unless the correct MPvar is set.
6277     *
6278     * @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!
6279     * @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 decend...)
6280     * @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'
6281     * @param bool $dontCheckEnableFields See function description
6282     * @param string $addSelectFields Additional fields to select. Syntax: ",[fieldname],[fieldname],...
6283     * @param string $moreWhereClauses Additional where clauses. Syntax: " AND [fieldname]=[value] AND ...
6284     * @param array $prevId_array array of IDs from previous recursions. In order to prevent infinite loops with mount pages.
6285     * @param int $recursionLevel Internal: Zero for the first recursion, incremented for each recursive call.
6286     * @return string Returns the list of ids as a comma separated string
6287     * @see TypoScriptFrontendController::checkEnableFields(), TypoScriptFrontendController::checkPagerecordForIncludeSection()
6288     */
6289    public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0)
6290    {
6291        $id = (int)$id;
6292        if (!$id) {
6293            return '';
6294        }
6295
6296        // Init vars:
6297        $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state' . $addSelectFields;
6298        $depth = (int)$depth;
6299        $begin = (int)$begin;
6300        $theList = [];
6301        $addId = 0;
6302        $requestHash = '';
6303
6304        // First level, check id (second level, this is done BEFORE the recursive call)
6305        $tsfe = $this->getTypoScriptFrontendController();
6306        if (!$recursionLevel) {
6307            // Check tree list cache
6308            // First, create the hash for this request - not sure yet whether we need all these parameters though
6309            $parameters = [
6310                $id,
6311                $depth,
6312                $begin,
6313                $dontCheckEnableFields,
6314                $addSelectFields,
6315                $moreWhereClauses,
6316                $prevId_array,
6317                GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1])
6318            ];
6319            $requestHash = md5(serialize($parameters));
6320            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
6321                ->getQueryBuilderForTable('cache_treelist');
6322            $cacheEntry = $queryBuilder->select('treelist')
6323                ->from('cache_treelist')
6324                ->where(
6325                    $queryBuilder->expr()->eq(
6326                        'md5hash',
6327                        $queryBuilder->createNamedParameter($requestHash, \PDO::PARAM_STR)
6328                    ),
6329                    $queryBuilder->expr()->orX(
6330                        $queryBuilder->expr()->gt(
6331                            'expires',
6332                            $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
6333                        ),
6334                        $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
6335                    )
6336                )
6337                ->setMaxResults(1)
6338                ->execute()
6339                ->fetch();
6340
6341            if (is_array($cacheEntry)) {
6342                // Cache hit
6343                return $cacheEntry['treelist'];
6344            }
6345            // If Id less than zero it means we should add the real id to list:
6346            if ($id < 0) {
6347                $addId = $id = abs($id);
6348            }
6349            // Check start page:
6350            if ($tsfe->sys_page->getRawRecord('pages', $id, 'uid')) {
6351                // Find mount point if any:
6352                $mount_info = $tsfe->sys_page->getMountPointInfo($id);
6353                if (is_array($mount_info)) {
6354                    $id = $mount_info['mount_pid'];
6355                    // In Overlay mode, use the mounted page uid as added ID!:
6356                    if ($addId && $mount_info['overlay']) {
6357                        $addId = $id;
6358                    }
6359                }
6360            } else {
6361                // Return blank if the start page was NOT found at all!
6362                return '';
6363            }
6364        }
6365        // Add this ID to the array of IDs
6366        if ($begin <= 0) {
6367            $prevId_array[] = $id;
6368        }
6369        // Select sublevel:
6370        if ($depth > 0) {
6371            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6372            $queryBuilder->getRestrictions()
6373                ->removeAll()
6374                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
6375            $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
6376                ->from('pages')
6377                ->where(
6378                    $queryBuilder->expr()->eq(
6379                        'pid',
6380                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6381                    ),
6382                    // tree is only built by language=0 pages
6383                    $queryBuilder->expr()->eq('sys_language_uid', 0)
6384                )
6385                ->orderBy('sorting');
6386
6387            if (!empty($moreWhereClauses)) {
6388                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
6389            }
6390
6391            $result = $queryBuilder->execute();
6392            while ($row = $result->fetch()) {
6393                /** @var VersionState $versionState */
6394                $versionState = VersionState::cast($row['t3ver_state']);
6395                $tsfe->sys_page->versionOL('pages', $row);
6396                if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
6397                    || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
6398                    || $versionState->indicatesPlaceholder()
6399                ) {
6400                    // Doing this after the overlay to make sure changes
6401                    // in the overlay are respected.
6402                    // However, we do not process pages below of and
6403                    // including of type recycler and BE user section
6404                    continue;
6405                }
6406                // Find mount point if any:
6407                $next_id = $row['uid'];
6408                $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row);
6409                // Overlay mode:
6410                if (is_array($mount_info) && $mount_info['overlay']) {
6411                    $next_id = $mount_info['mount_pid'];
6412                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
6413                        ->getQueryBuilderForTable('pages');
6414                    $queryBuilder->getRestrictions()
6415                        ->removeAll()
6416                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
6417                    $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
6418                        ->from('pages')
6419                        ->where(
6420                            $queryBuilder->expr()->eq(
6421                                'uid',
6422                                $queryBuilder->createNamedParameter($next_id, \PDO::PARAM_INT)
6423                            )
6424                        )
6425                        ->orderBy('sorting')
6426                        ->setMaxResults(1);
6427
6428                    if (!empty($moreWhereClauses)) {
6429                        $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
6430                    }
6431
6432                    $row = $queryBuilder->execute()->fetch();
6433                    $tsfe->sys_page->versionOL('pages', $row);
6434                    if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
6435                        || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
6436                        || $versionState->indicatesPlaceholder()
6437                    ) {
6438                        // Doing this after the overlay to make sure
6439                        // changes in the overlay are respected.
6440                        // see above
6441                        continue;
6442                    }
6443                }
6444                // Add record:
6445                if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) {
6446                    // Add ID to list:
6447                    if ($begin <= 0) {
6448                        if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) {
6449                            $theList[] = $next_id;
6450                        }
6451                    }
6452                    // Next level:
6453                    if ($depth > 1 && !$row['php_tree_stop']) {
6454                        // Normal mode:
6455                        if (is_array($mount_info) && !$mount_info['overlay']) {
6456                            $next_id = $mount_info['mount_pid'];
6457                        }
6458                        // Call recursively, if the id is not in prevID_array:
6459                        if (!in_array($next_id, $prevId_array)) {
6460                            $theList = array_merge(
6461                                GeneralUtility::intExplode(
6462                                    ',',
6463                                    $this->getTreeList(
6464                                        $next_id,
6465                                        $depth - 1,
6466                                        $begin - 1,
6467                                        $dontCheckEnableFields,
6468                                        $addSelectFields,
6469                                        $moreWhereClauses,
6470                                        $prevId_array,
6471                                        $recursionLevel + 1
6472                                    ),
6473                                    true
6474                                ),
6475                                $theList
6476                            );
6477                        }
6478                    }
6479                }
6480            }
6481        }
6482        // If first run, check if the ID should be returned:
6483        if (!$recursionLevel) {
6484            if ($addId) {
6485                if ($begin > 0) {
6486                    $theList[] = 0;
6487                } else {
6488                    $theList[] = $addId;
6489                }
6490            }
6491
6492            $cacheEntry = [
6493                'md5hash' => $requestHash,
6494                'pid' => $id,
6495                'treelist' => implode(',', $theList),
6496                'tstamp' => $GLOBALS['EXEC_TIME'],
6497            ];
6498
6499            // Only add to cache if not logged into TYPO3 Backend
6500            if (!$this->getFrontendBackendUser() instanceof AbstractUserAuthentication) {
6501                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
6502                try {
6503                    $connection->transactional(function ($connection) use ($cacheEntry) {
6504                        $connection->insert('cache_treelist', $cacheEntry);
6505                    });
6506                } catch (\Throwable $e) {
6507                }
6508            }
6509        }
6510
6511        return implode(',', $theList);
6512    }
6513
6514    /**
6515     * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.)
6516     * 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%")'
6517     *
6518     * @param string $searchWords The search words. These will be separated by space and comma.
6519     * @param string $searchFieldList The fields to search in
6520     * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well)
6521     * @return string The WHERE clause.
6522     */
6523    public function searchWhere($searchWords, $searchFieldList, $searchTable)
6524    {
6525        if (!$searchWords) {
6526            return '';
6527        }
6528
6529        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
6530            ->getQueryBuilderForTable($searchTable);
6531
6532        $prefixTableName = $searchTable ? $searchTable . '.' : '';
6533
6534        $where = $queryBuilder->expr()->andX();
6535        $searchFields = explode(',', $searchFieldList);
6536        $searchWords = preg_split('/[ ,]/', $searchWords);
6537        foreach ($searchWords as $searchWord) {
6538            $searchWord = trim($searchWord);
6539            if (strlen($searchWord) < 3) {
6540                continue;
6541            }
6542            $searchWordConstraint = $queryBuilder->expr()->orX();
6543            $searchWord = $queryBuilder->escapeLikeWildcards($searchWord);
6544            foreach ($searchFields as $field) {
6545                $searchWordConstraint->add(
6546                    $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%'))
6547                );
6548            }
6549
6550            if ($searchWordConstraint->count()) {
6551                $where->add($searchWordConstraint);
6552            }
6553        }
6554
6555        if ((string)$where === '') {
6556            return '';
6557        }
6558
6559        return ' AND (' . (string)$where . ')';
6560    }
6561
6562    /**
6563     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
6564     * This function is preferred over ->getQuery() if you just need to create and then execute a query.
6565     *
6566     * @param string $table The table name
6567     * @param array $conf The TypoScript configuration properties
6568     * @return Statement
6569     * @see getQuery()
6570     */
6571    public function exec_getQuery($table, $conf)
6572    {
6573        $statement = $this->getQuery($table, $conf);
6574        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6575
6576        return $connection->executeQuery($statement);
6577    }
6578
6579    /**
6580     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
6581     * and overlays with translation and version if available
6582     *
6583     * @param string $tableName the name of the TCA database table
6584     * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference
6585     * @return array The records
6586     * @throws \UnexpectedValueException
6587     */
6588    public function getRecords($tableName, array $queryConfiguration)
6589    {
6590        $records = [];
6591
6592        $statement = $this->exec_getQuery($tableName, $queryConfiguration);
6593
6594        $tsfe = $this->getTypoScriptFrontendController();
6595        while ($row = $statement->fetch()) {
6596            // Versioning preview:
6597            $tsfe->sys_page->versionOL($tableName, $row, true);
6598
6599            // Language overlay:
6600            if (is_array($row)) {
6601                $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row);
6602            }
6603
6604            // Might be unset in the language overlay
6605            if (is_array($row)) {
6606                $records[] = $row;
6607            }
6608        }
6609
6610        return $records;
6611    }
6612
6613    /**
6614     * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array
6615     * Implements the "select" function in TypoScript
6616     *
6617     * @param string $table See ->exec_getQuery()
6618     * @param array $conf See ->exec_getQuery()
6619     * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED!
6620     * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts.
6621     * @throws \RuntimeException
6622     * @throws \InvalidArgumentException
6623     * @internal
6624     * @see CONTENT(), numRows()
6625     */
6626    public function getQuery($table, $conf, $returnQueryArray = false)
6627    {
6628        // Resolve stdWrap in these properties first
6629        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6630        $properties = [
6631            'pidInList',
6632            'uidInList',
6633            'languageField',
6634            'selectFields',
6635            'max',
6636            'begin',
6637            'groupBy',
6638            'orderBy',
6639            'join',
6640            'leftjoin',
6641            'rightjoin',
6642            'recursive',
6643            'where'
6644        ];
6645        foreach ($properties as $property) {
6646            $conf[$property] = trim(
6647                isset($conf[$property . '.'])
6648                    ? $this->stdWrap($conf[$property], $conf[$property . '.'])
6649                    : $conf[$property]
6650            );
6651            if ($conf[$property] === '') {
6652                unset($conf[$property]);
6653            } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) {
6654                $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]);
6655            }
6656            if (isset($conf[$property . '.'])) {
6657                // stdWrapping already done, so remove the sub-array
6658                unset($conf[$property . '.']);
6659            }
6660        }
6661        // Handle PDO-style named parameter markers first
6662        $queryMarkers = $this->getQueryMarkers($table, $conf);
6663        // Replace the markers in the non-stdWrap properties
6664        foreach ($queryMarkers as $marker => $markerValue) {
6665            $properties = [
6666                'uidInList',
6667                'selectFields',
6668                'where',
6669                'max',
6670                'begin',
6671                'groupBy',
6672                'orderBy',
6673                'join',
6674                'leftjoin',
6675                'rightjoin'
6676            ];
6677            foreach ($properties as $property) {
6678                if ($conf[$property]) {
6679                    $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]);
6680                }
6681            }
6682        }
6683
6684        // Construct WHERE clause:
6685        // Handle recursive function for the pidInList
6686        if (isset($conf['recursive'])) {
6687            $conf['recursive'] = (int)$conf['recursive'];
6688            if ($conf['recursive'] > 0) {
6689                $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true);
6690                array_walk($pidList, function (&$storagePid) {
6691                    if ($storagePid === 'this') {
6692                        $storagePid = $this->getTypoScriptFrontendController()->id;
6693                    }
6694                    if ($storagePid > 0) {
6695                        $storagePid = -$storagePid;
6696                    }
6697                });
6698                $expandedPidList = [];
6699                foreach ($pidList as $value) {
6700                    // Implementation of getTreeList allows to pass the id negative to include
6701                    // it into the result otherwise only childpages are returned
6702                    $expandedPidList = array_merge(
6703                        GeneralUtility::intExplode(',', $this->getTreeList($value, $conf['recursive'])),
6704                        $expandedPidList
6705                    );
6706                }
6707                $conf['pidInList'] = implode(',', $expandedPidList);
6708            }
6709        }
6710        if ((string)$conf['pidInList'] === '') {
6711            $conf['pidInList'] = 'this';
6712        }
6713
6714        $queryParts = $this->getQueryConstraints($table, $conf);
6715
6716        $queryBuilder = $connection->createQueryBuilder();
6717        // @todo Check against getQueryConstraints, can probably use FrontendRestrictions
6718        // @todo here and remove enableFields there.
6719        $queryBuilder->getRestrictions()->removeAll();
6720        $queryBuilder->select('*')->from($table);
6721
6722        if ($queryParts['where']) {
6723            $queryBuilder->where($queryParts['where']);
6724        }
6725
6726        if ($queryParts['groupBy']) {
6727            $queryBuilder->groupBy(...$queryParts['groupBy']);
6728        }
6729
6730        if (is_array($queryParts['orderBy'])) {
6731            foreach ($queryParts['orderBy'] as $orderBy) {
6732                $queryBuilder->addOrderBy(...$orderBy);
6733            }
6734        }
6735
6736        // Fields:
6737        if ($conf['selectFields']) {
6738            $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table));
6739        }
6740
6741        // Setting LIMIT:
6742        $error = false;
6743        if ($conf['max'] || $conf['begin']) {
6744            // Finding the total number of records, if used:
6745            if (strpos(strtolower($conf['begin'] . $conf['max']), 'total') !== false) {
6746                $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6747                $countQueryBuilder->getRestrictions()->removeAll();
6748                $countQueryBuilder->count('*')
6749                    ->from($table)
6750                    ->where($queryParts['where']);
6751
6752                if ($queryParts['groupBy']) {
6753                    $countQueryBuilder->groupBy(...$queryParts['groupBy']);
6754                }
6755
6756                try {
6757                    $count = $countQueryBuilder->execute()->fetchColumn(0);
6758                    $conf['max'] = str_ireplace('total', $count, $conf['max']);
6759                    $conf['begin'] = str_ireplace('total', $count, $conf['begin']);
6760                } catch (DBALException $e) {
6761                    $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage());
6762                    $error = true;
6763                }
6764            }
6765
6766            if (!$error) {
6767                $conf['begin'] = MathUtility::forceIntegerInRange(ceil($this->calc($conf['begin'])), 0);
6768                $conf['max'] = MathUtility::forceIntegerInRange(ceil($this->calc($conf['max'])), 0);
6769                if ($conf['begin'] > 0) {
6770                    $queryBuilder->setFirstResult($conf['begin']);
6771                }
6772                $queryBuilder->setMaxResults($conf['max'] ?: 100000);
6773            }
6774        }
6775
6776        if (!$error) {
6777            // Setting up tablejoins:
6778            if ($conf['join']) {
6779                $joinParts = QueryHelper::parseJoin($conf['join']);
6780                $queryBuilder->join(
6781                    $table,
6782                    $joinParts['tableName'],
6783                    $joinParts['tableAlias'],
6784                    $joinParts['joinCondition']
6785                );
6786            } elseif ($conf['leftjoin']) {
6787                $joinParts = QueryHelper::parseJoin($conf['leftjoin']);
6788                $queryBuilder->leftJoin(
6789                    $table,
6790                    $joinParts['tableName'],
6791                    $joinParts['tableAlias'],
6792                    $joinParts['joinCondition']
6793                );
6794            } elseif ($conf['rightjoin']) {
6795                $joinParts = QueryHelper::parseJoin($conf['rightjoin']);
6796                $queryBuilder->rightJoin(
6797                    $table,
6798                    $joinParts['tableName'],
6799                    $joinParts['tableAlias'],
6800                    $joinParts['joinCondition']
6801                );
6802            }
6803
6804            // Convert the QueryBuilder object into a SQL statement.
6805            $query = $queryBuilder->getSQL();
6806
6807            // Replace the markers in the queryParts to handle stdWrap enabled properties
6808            foreach ($queryMarkers as $marker => $markerValue) {
6809                // @todo Ugly hack that needs to be cleaned up, with the current architecture
6810                // @todo for exec_Query / getQuery it's the best we can do.
6811                $query = str_replace('###' . $marker . '###', $markerValue, $query);
6812                foreach ($queryParts as $queryPartKey => &$queryPartValue) {
6813                    $queryPartValue = str_replace('###' . $marker . '###', $markerValue, $queryPartValue);
6814                }
6815                unset($queryPartValue);
6816            }
6817
6818            return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
6819        }
6820
6821        return '';
6822    }
6823
6824    /**
6825     * Helper to transform a QueryBuilder object into a queryParts array that can be used
6826     * with exec_SELECT_queryArray
6827     *
6828     * @param \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder
6829     * @return array
6830     * @throws \RuntimeException
6831     */
6832    protected function getQueryArray(QueryBuilder $queryBuilder)
6833    {
6834        $fromClauses = [];
6835        $knownAliases = [];
6836        $queryParts = [];
6837
6838        // Loop through all FROM clauses
6839        foreach ($queryBuilder->getQueryPart('from') as $from) {
6840            if ($from['alias'] === null) {
6841                $tableSql = $from['table'];
6842                $tableReference = $from['table'];
6843            } else {
6844                $tableSql = $from['table'] . ' ' . $from['alias'];
6845                $tableReference = $from['alias'];
6846            }
6847
6848            $knownAliases[$tableReference] = true;
6849
6850            $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
6851                $tableReference,
6852                $queryBuilder->getQueryPart('join'),
6853                $knownAliases
6854            );
6855        }
6856
6857        $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
6858        $queryParts['FROM'] = implode(', ', $fromClauses);
6859        $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
6860        $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
6861        $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
6862        if ($queryBuilder->getFirstResult() > 0) {
6863            $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
6864        } elseif ($queryBuilder->getMaxResults() > 0) {
6865            $queryParts['LIMIT'] = $queryBuilder->getMaxResults();
6866        }
6867
6868        return $queryParts;
6869    }
6870
6871    /**
6872     * Helper to transform the QueryBuilder join part into a SQL fragment.
6873     *
6874     * @param string $fromAlias
6875     * @param array $joinParts
6876     * @param array $knownAliases
6877     * @return string
6878     * @throws \RuntimeException
6879     */
6880    protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
6881    {
6882        $sql = '';
6883
6884        if (isset($joinParts['join'][$fromAlias])) {
6885            foreach ($joinParts['join'][$fromAlias] as $join) {
6886                if (array_key_exists($join['joinAlias'], $knownAliases)) {
6887                    throw new \RuntimeException(
6888                        'Non unique join alias: "' . $join['joinAlias'] . '" found.',
6889                        1472748872
6890                    );
6891                }
6892                $sql .= ' ' . strtoupper($join['joinType'])
6893                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
6894                    . ' ON ' . ((string)$join['joinCondition']);
6895                $knownAliases[$join['joinAlias']] = true;
6896            }
6897
6898            foreach ($joinParts['join'][$fromAlias] as $join) {
6899                $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
6900            }
6901        }
6902
6903        return $sql;
6904    }
6905    /**
6906     * Helper function for getQuery(), creating the WHERE clause of the SELECT query
6907     *
6908     * @param string $table The table name
6909     * @param array $conf The TypoScript configuration properties
6910     * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments
6911     * @throws \InvalidArgumentException
6912     * @see getQuery()
6913     */
6914    protected function getQueryConstraints(string $table, array $conf): array
6915    {
6916        // Init:
6917        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6918        $expressionBuilder = $queryBuilder->expr();
6919        $tsfe = $this->getTypoScriptFrontendController();
6920        $constraints = [];
6921        $pid_uid_flag = 0;
6922        $enableFieldsIgnore = [];
6923        $queryParts = [
6924            'where' => null,
6925            'groupBy' => null,
6926            'orderBy' => null,
6927        ];
6928
6929        $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
6930        $considerMovePlaceholders = (
6931            $isInWorkspace && $table !== 'pages'
6932            && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
6933        );
6934
6935        if (trim($conf['uidInList'])) {
6936            $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $conf['uidInList']));
6937
6938            // If move placeholder shall be considered, select via t3ver_move_id
6939            if ($considerMovePlaceholders) {
6940                $constraints[] = (string)$expressionBuilder->orX(
6941                    $expressionBuilder->in($table . '.uid', $listArr),
6942                    $expressionBuilder->andX(
6943                        $expressionBuilder->eq(
6944                            $table . '.t3ver_state',
6945                            (int)(string)VersionState::cast(VersionState::MOVE_PLACEHOLDER)
6946                        ),
6947                        $expressionBuilder->in($table . '.t3ver_move_id', $listArr)
6948                    )
6949                );
6950            } else {
6951                $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr);
6952            }
6953            $pid_uid_flag++;
6954        }
6955
6956        // Static_* tables are allowed to be fetched from root page
6957        if (strpos($table, 'static_') === 0) {
6958            $pid_uid_flag++;
6959        }
6960
6961        if (trim($conf['pidInList'])) {
6962            $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $conf['pidInList']));
6963            // Removes all pages which are not visible for the user!
6964            $listArr = $this->checkPidArray($listArr);
6965            if (GeneralUtility::inList($conf['pidInList'], 'root')) {
6966                $listArr[] = 0;
6967            }
6968            if (GeneralUtility::inList($conf['pidInList'], '-1')) {
6969                $listArr[] = -1;
6970                $enableFieldsIgnore['pid'] = true;
6971            }
6972            if (!empty($listArr)) {
6973                $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr));
6974                $pid_uid_flag++;
6975            } else {
6976                // If not uid and not pid then uid is set to 0 - which results in nothing!!
6977                $pid_uid_flag = 0;
6978            }
6979        }
6980
6981        // If not uid and not pid then uid is set to 0 - which results in nothing!!
6982        if (!$pid_uid_flag) {
6983            $constraints[] = $expressionBuilder->eq($table . '.uid', 0);
6984        }
6985
6986        $where = isset($conf['where.']) ? trim($this->stdWrap($conf['where'], $conf['where.'])) : trim($conf['where']);
6987        if ($where) {
6988            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
6989        }
6990
6991        // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched
6992        // but only do this for TCA tables that have languages enabled
6993        $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class));
6994        if ($languageConstraint !== null) {
6995            $constraints[] = $languageConstraint;
6996        }
6997
6998        // Enablefields
6999        if ($table === 'pages') {
7000            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
7001            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
7002        } else {
7003            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
7004        }
7005
7006        // MAKE WHERE:
7007        if (count($constraints) !== 0) {
7008            $queryParts['where'] = $expressionBuilder->andX(...$constraints);
7009        }
7010        // GROUP BY
7011        if (trim($conf['groupBy'])) {
7012            $groupBy = isset($conf['groupBy.'])
7013                ? trim($this->stdWrap($conf['groupBy'], $conf['groupBy.']))
7014                : trim($conf['groupBy']);
7015            $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy);
7016        }
7017
7018        // ORDER BY
7019        if (trim($conf['orderBy'])) {
7020            $orderByString = isset($conf['orderBy.'])
7021                ? trim($this->stdWrap($conf['orderBy'], $conf['orderBy.']))
7022                : trim($conf['orderBy']);
7023
7024            $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString);
7025        }
7026
7027        // Return result:
7028        return $queryParts;
7029    }
7030
7031    /**
7032     * Adds parts to the WHERE clause that are related to language.
7033     * This only works on TCA tables which have the [ctrl][languageField] field set or if they
7034     * have select.languageField = my_language_field set explicitly.
7035     *
7036     * It is also possible to disable the language restriction for a query by using select.languageField = 0,
7037     * if select.languageField is not explicitly set, the TCA default values are taken.
7038     *
7039     * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted:
7040     *
7041     * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are
7042     * fetched (the overlays are taken care of later-on).
7043     * if the current language has overlays but also records without localization-parent (free mode) available,
7044     * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1
7045     * which overrules the overlayType within the language aspect.
7046     *
7047     * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records
7048     * for the current language.
7049     *
7050     * @param ExpressionBuilder $expressionBuilder
7051     * @param string $table
7052     * @param array $conf
7053     * @param Context $context
7054     * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null
7055     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
7056     */
7057    protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
7058    {
7059        $languageField = '';
7060        $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
7061        // Check if the table is translatable, and set the language field by default from the TCA information
7062        if (!empty($conf['languageField']) || !isset($conf['languageField'])) {
7063            if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) {
7064                $languageField = $conf['languageField'];
7065            } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) {
7066                $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
7067            }
7068        }
7069
7070        // No language restriction enabled explicitly or available via TCA
7071        if (empty($languageField)) {
7072            return null;
7073        }
7074
7075        /** @var LanguageAspect $languageAspect */
7076        $languageAspect = $context->getAspect('language');
7077        if ($languageAspect->doOverlays() && !empty($localizationParentField)) {
7078            // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will
7079            // OVERLAY the records with localized versions!
7080            $languageQuery = $expressionBuilder->in($languageField, [0, -1]);
7081            // Use this option to include records that don't have a default language counterpart ("free mode")
7082            // (originalpointerfield is 0 and the language field contains the requested language)
7083            if (isset($conf['includeRecordsWithoutDefaultTranslation']) || $conf['includeRecordsWithoutDefaultTranslation.']) {
7084                $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ?
7085                    $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation'];
7086                $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== '';
7087            } else {
7088                // Option was not explicitly set, check what's in for the language overlay type.
7089                $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING;
7090            }
7091            if ($includeRecordsWithoutDefaultTranslation) {
7092                $languageQuery = $expressionBuilder->orX(
7093                    $languageQuery,
7094                    $expressionBuilder->andX(
7095                        $expressionBuilder->eq($table . '.' . $localizationParentField, 0),
7096                        $expressionBuilder->eq($languageField, $languageAspect->getContentId())
7097                    )
7098                );
7099            }
7100            return $languageQuery;
7101        }
7102        // No overlays = only fetch records given for the requested language and "all languages"
7103        return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]);
7104    }
7105
7106    /**
7107     * Helper function for getQuery, sanitizing the select part
7108     *
7109     * This functions checks if the necessary fields are part of the select
7110     * and adds them if necessary.
7111     *
7112     * @param string $selectPart Select part
7113     * @param string $table Table to select from
7114     * @return string Sanitized select part
7115     * @internal
7116     * @see getQuery
7117     */
7118    protected function sanitizeSelectPart($selectPart, $table)
7119    {
7120        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7121
7122        // Pattern matching parts
7123        $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
7124        $matchEnd = '(\\s*,|\\s*$)/';
7125        $necessaryFields = ['uid', 'pid'];
7126        $wsFields = ['t3ver_state'];
7127        if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) {
7128            foreach ($necessaryFields as $field) {
7129                $match = $matchStart . $field . $matchEnd;
7130                if (!preg_match($match, $selectPart)) {
7131                    $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
7132                }
7133            }
7134            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
7135                foreach ($wsFields as $field) {
7136                    $match = $matchStart . $field . $matchEnd;
7137                    if (!preg_match($match, $selectPart)) {
7138                        $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
7139                    }
7140                }
7141            }
7142        }
7143        return $selectPart;
7144    }
7145
7146    /**
7147     * 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)
7148     *
7149     * @param array $listArr Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed.
7150     * @return array Returns the array of remaining page UID numbers
7151     * @internal
7152     * @see checkPid()
7153     */
7154    public function checkPidArray($listArr)
7155    {
7156        if (!is_array($listArr) || empty($listArr)) {
7157            return [];
7158        }
7159        $outArr = [];
7160        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7161        $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
7162        $queryBuilder->select('uid')
7163            ->from('pages')
7164            ->where(
7165                $queryBuilder->expr()->in(
7166                    'uid',
7167                    $queryBuilder->createNamedParameter($listArr, Connection::PARAM_INT_ARRAY)
7168                ),
7169                $queryBuilder->expr()->notIn(
7170                    'doktype',
7171                    $queryBuilder->createNamedParameter(
7172                        GeneralUtility::intExplode(',', $this->checkPid_badDoktypeList, true),
7173                        Connection::PARAM_INT_ARRAY
7174                    )
7175                )
7176            );
7177        try {
7178            $result = $queryBuilder->execute();
7179            while ($row = $result->fetch()) {
7180                $outArr[] = $row['uid'];
7181            }
7182        } catch (DBALException $e) {
7183            $this->getTimeTracker()->setTSlogMessage($e->getMessage() . ': ' . $queryBuilder->getSQL(), 3);
7184        }
7185
7186        return $outArr;
7187    }
7188
7189    /**
7190     * Checks if a page UID is available due to enableFields() AND the list of bad doktype numbers ($this->checkPid_badDoktypeList)
7191     *
7192     * @param int $uid Page UID to test
7193     * @return bool TRUE if OK
7194     * @internal
7195     * @see checkPidArray()
7196     */
7197    public function checkPid($uid)
7198    {
7199        $uid = (int)$uid;
7200        if (!isset($this->checkPid_cache[$uid])) {
7201            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7202            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
7203            $count = $queryBuilder->count('*')
7204                ->from('pages')
7205                ->where(
7206                    $queryBuilder->expr()->eq(
7207                        'uid',
7208                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
7209                    ),
7210                    $queryBuilder->expr()->notIn(
7211                        'doktype',
7212                        $queryBuilder->createNamedParameter(
7213                            GeneralUtility::intExplode(',', $this->checkPid_badDoktypeList, true),
7214                            Connection::PARAM_INT_ARRAY
7215                        )
7216                    )
7217                )
7218                ->execute()
7219                ->fetchColumn(0);
7220
7221            $this->checkPid_cache[$uid] = (bool)$count;
7222        }
7223        return $this->checkPid_cache[$uid];
7224    }
7225
7226    /**
7227     * Builds list of marker values for handling PDO-like parameter markers in select parts.
7228     * 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.
7229     *
7230     * @param string $table Table to select records from
7231     * @param array $conf Select part of CONTENT definition
7232     * @return array List of values to replace markers with
7233     * @internal
7234     * @see getQuery()
7235     */
7236    public function getQueryMarkers($table, $conf)
7237    {
7238        if (!is_array($conf['markers.'])) {
7239            return [];
7240        }
7241        // Parse markers and prepare their values
7242        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7243        $markerValues = [];
7244        foreach ($conf['markers.'] as $dottedMarker => $dummy) {
7245            $marker = rtrim($dottedMarker, '.');
7246            if ($dottedMarker != $marker . '.') {
7247                continue;
7248            }
7249            // Parse definition
7250            $tempValue = isset($conf['markers.'][$dottedMarker])
7251                ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'], $conf['markers.'][$dottedMarker])
7252                : $conf['markers.'][$dottedMarker]['value'];
7253            // Quote/escape if needed
7254            if (is_numeric($tempValue)) {
7255                if ((int)$tempValue == $tempValue) {
7256                    // Handle integer
7257                    $markerValues[$marker] = (int)$tempValue;
7258                } else {
7259                    // Handle float
7260                    $markerValues[$marker] = (float)$tempValue;
7261                }
7262            } elseif ($tempValue === null) {
7263                // It represents NULL
7264                $markerValues[$marker] = 'NULL';
7265            } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
7266                // See if it is really a comma separated list of values
7267                $explodeValues = GeneralUtility::trimExplode(',', $tempValue);
7268                if (count($explodeValues) > 1) {
7269                    // Handle each element of list separately
7270                    $tempArray = [];
7271                    foreach ($explodeValues as $listValue) {
7272                        if (is_numeric($listValue)) {
7273                            if ((int)$listValue == $listValue) {
7274                                $tempArray[] = (int)$listValue;
7275                            } else {
7276                                $tempArray[] = (float)$listValue;
7277                            }
7278                        } else {
7279                            // If quoted, remove quotes before
7280                            // escaping.
7281                            if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
7282                                $listValue = $matches[1];
7283                            } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
7284                                $listValue = $matches[1];
7285                            }
7286                            $tempArray[] = $connection->quote($listValue);
7287                        }
7288                    }
7289                    $markerValues[$marker] = implode(',', $tempArray);
7290                } else {
7291                    // Handle remaining values as string
7292                    $markerValues[$marker] = $connection->quote($tempValue);
7293                }
7294            } else {
7295                // Handle remaining values as string
7296                $markerValues[$marker] = $connection->quote($tempValue);
7297            }
7298        }
7299        return $markerValues;
7300    }
7301
7302    /***********************************************
7303     *
7304     * Frontend editing functions
7305     *
7306     ***********************************************/
7307    /**
7308     * 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.
7309     * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
7310     * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
7311     *
7312     * @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.
7313     * @param array $conf TypoScript configuration properties for the editPanel
7314     * @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
7315     * @param array $dataArray Alternative data array to use. Default is $this->data
7316     * @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.
7317     */
7318    public function editPanel($content, $conf, $currentRecord = '', $dataArray = [])
7319    {
7320        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
7321            return $content;
7322        }
7323        if (!$this->getTypoScriptFrontendController()->displayEditIcons) {
7324            return $content;
7325        }
7326
7327        if (!$currentRecord) {
7328            $currentRecord = $this->currentRecord;
7329        }
7330        if (empty($dataArray)) {
7331            $dataArray = $this->data;
7332        }
7333
7334        if ($conf['newRecordFromTable']) {
7335            $currentRecord = $conf['newRecordFromTable'] . ':NEW';
7336            $conf['allow'] = 'new';
7337            $checkEditAccessInternals = false;
7338        } else {
7339            $checkEditAccessInternals = true;
7340        }
7341        list($table, $uid) = explode(':', $currentRecord);
7342        // Page ID for new records, 0 if not specified
7343        $newRecordPid = (int)$conf['newRecordInPid'];
7344        $newUid = null;
7345        if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) {
7346            if ($table === 'pages') {
7347                $newUid = $uid;
7348            } else {
7349                if ($conf['newRecordFromTable']) {
7350                    $newUid = $this->getTypoScriptFrontendController()->id;
7351                    if ($newRecordPid) {
7352                        $newUid = $newRecordPid;
7353                    }
7354                } else {
7355                    $newUid = -1 * $uid;
7356                }
7357            }
7358        }
7359        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
7360            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
7361            if ($editClass) {
7362                $edit = GeneralUtility::makeInstance($editClass);
7363                $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']);
7364                $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []);
7365            }
7366        }
7367        return $content;
7368    }
7369
7370    /**
7371     * 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.
7372     * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
7373     *
7374     * @param string $content The content to which the edit icons should be appended
7375     * @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
7376     * @param array $conf TypoScript properties for configuring the edit icons.
7377     * @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
7378     * @param array $dataArray Alternative data array to use. Default is $this->data
7379     * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine
7380     * @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.
7381     */
7382    public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '')
7383    {
7384        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
7385            return $content;
7386        }
7387        if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) {
7388            return $content;
7389        }
7390        if (!$currentRecord) {
7391            $currentRecord = $this->currentRecord;
7392        }
7393        if (empty($dataArray)) {
7394            $dataArray = $this->data;
7395        }
7396        // Check incoming params:
7397        list($currentRecordTable, $currentRecordUID) = explode(':', $currentRecord);
7398        list($fieldList, $table) = array_reverse(GeneralUtility::trimExplode(':', $params, true));
7399        // Reverse the array because table is optional
7400        if (!$table) {
7401            $table = $currentRecordTable;
7402        } elseif ($table != $currentRecordTable) {
7403            // If the table is set as the first parameter, and does not match the table of the current record, then just return.
7404            return $content;
7405        }
7406
7407        $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID;
7408        // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
7409        if (!array_key_exists('allow', $conf)) {
7410            $conf['allow'] = 'edit';
7411        }
7412        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
7413            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
7414            if ($editClass) {
7415                $edit = GeneralUtility::makeInstance($editClass);
7416                $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
7417            }
7418        }
7419        return $content;
7420    }
7421
7422    /**
7423     * Returns TRUE if the input table/row would be hidden in the frontend (according nto the current time and simulate user group)
7424     *
7425     * @param string $table The table name
7426     * @param array $row The data record
7427     * @return bool
7428     * @internal
7429     * @see editPanelPreviewBorder()
7430     */
7431    public function isDisabled($table, $row)
7432    {
7433        $tsfe = $this->getTypoScriptFrontendController();
7434        $enablecolumns = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
7435        return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']]
7436            || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup
7437            || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > $GLOBALS['EXEC_TIME']
7438            || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < $GLOBALS['EXEC_TIME'];
7439    }
7440
7441    /**
7442     * Get instance of FAL resource factory
7443     *
7444     * @return ResourceFactory
7445     */
7446    protected function getResourceFactory()
7447    {
7448        return ResourceFactory::getInstance();
7449    }
7450
7451    /**
7452     * Wrapper function for GeneralUtility::getIndpEnv()
7453     *
7454     * @see GeneralUtility::getIndpEnv
7455     * @param string $key Name of the "environment variable"/"server variable" you wish to get.
7456     * @return string
7457     */
7458    protected function getEnvironmentVariable($key)
7459    {
7460        return GeneralUtility::getIndpEnv($key);
7461    }
7462
7463    /**
7464     * Fetches content from cache
7465     *
7466     * @param array $configuration Array
7467     * @return string|bool FALSE on cache miss
7468     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
7469     */
7470    protected function getFromCache(array $configuration)
7471    {
7472        $content = false;
7473
7474        if ($this->getTypoScriptFrontendController()->no_cache) {
7475            return $content;
7476        }
7477        $cacheKey = $this->calculateCacheKey($configuration);
7478        if (!empty($cacheKey)) {
7479            /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
7480            $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)
7481                ->getCache('cache_hash');
7482            $content = $cacheFrontend->get($cacheKey);
7483        }
7484        return $content;
7485    }
7486
7487    /**
7488     * Calculates the lifetime of a cache entry based on the given configuration
7489     *
7490     * @param array $configuration
7491     * @return int|null
7492     */
7493    protected function calculateCacheLifetime(array $configuration)
7494    {
7495        $lifetimeConfiguration = $configuration['lifetime'] ?? '';
7496        $lifetimeConfiguration = isset($configuration['lifetime.'])
7497            ? $this->stdWrap($lifetimeConfiguration, $configuration['lifetime.'])
7498            : $lifetimeConfiguration;
7499
7500        $lifetime = null; // default lifetime
7501        if (strtolower($lifetimeConfiguration) === 'unlimited') {
7502            $lifetime = 0; // unlimited
7503        } elseif ($lifetimeConfiguration > 0) {
7504            $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds
7505        }
7506        return $lifetime;
7507    }
7508
7509    /**
7510     * Calculates the tags for a cache entry bases on the given configuration
7511     *
7512     * @param array $configuration
7513     * @return array
7514     */
7515    protected function calculateCacheTags(array $configuration)
7516    {
7517        $tags = $configuration['tags'] ?? '';
7518        $tags = isset($configuration['tags.'])
7519            ? $this->stdWrap($tags, $configuration['tags.'])
7520            : $tags;
7521        return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags);
7522    }
7523
7524    /**
7525     * Applies stdWrap to the cache key
7526     *
7527     * @param array $configuration
7528     * @return string
7529     */
7530    protected function calculateCacheKey(array $configuration)
7531    {
7532        $key = $configuration['key'] ?? '';
7533        return isset($configuration['key.'])
7534            ? $this->stdWrap($key, $configuration['key.'])
7535            : $key;
7536    }
7537
7538    /**
7539     * Returns the current BE user.
7540     *
7541     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
7542     */
7543    protected function getFrontendBackendUser()
7544    {
7545        return $GLOBALS['BE_USER'];
7546    }
7547
7548    /**
7549     * @return TimeTracker
7550     */
7551    protected function getTimeTracker()
7552    {
7553        return GeneralUtility::makeInstance(TimeTracker::class);
7554    }
7555
7556    /**
7557     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
7558     */
7559    protected function getTypoScriptFrontendController()
7560    {
7561        return $this->typoScriptFrontendController ?: $GLOBALS['TSFE'];
7562    }
7563
7564    /**
7565     * Support anchors without href value
7566     * Changes ContentObjectRenderer::typolink to render a tag without href,
7567     * if id or name attribute is present.
7568     *
7569     * @param string $linkText
7570     * @param array $conf Typolink configuration decoded as array
7571     * @return string Full a-Tag or just the linktext if id or name are not set.
7572     */
7573    protected function resolveAnchorLink(string $linkText, array $conf): string
7574    {
7575        $anchorTag = '<a ' . $this->getATagParams($conf) . '>';
7576        $aTagParams = GeneralUtility::get_tag_attributes($anchorTag);
7577        // If it looks like a anchor tag, render it anyway
7578        if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
7579            return $anchorTag . $linkText . '</a>';
7580        }
7581        // Otherwise just return the link text
7582        return $linkText;
7583    }
7584
7585    protected function shallDebug(): bool
7586    {
7587        $tsfe = $this->getTypoScriptFrontendController();
7588        if ($tsfe !== null && isset($tsfe->config['config']['debug'])) {
7589            return (bool)($tsfe->config['config']['debug']);
7590        }
7591        return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
7592    }
7593}
7594