Completed
Push — master ( 1106bd...d9b8ee )
by
unknown
18:22
created

ContentObjectRenderer::stdWrap_listNum()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace 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
17
use Doctrine\DBAL\DBALException;
18
use Doctrine\DBAL\Driver\Statement;
19
use Psr\Container\ContainerInterface;
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerAwareTrait;
22
use Symfony\Component\Mime\Address;
23
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
24
use TYPO3\CMS\Core\Cache\CacheManager;
25
use TYPO3\CMS\Core\Context\Context;
26
use TYPO3\CMS\Core\Context\LanguageAspect;
27
use TYPO3\CMS\Core\Core\Environment;
28
use TYPO3\CMS\Core\Database\Connection;
29
use TYPO3\CMS\Core\Database\ConnectionPool;
30
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
31
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32
use TYPO3\CMS\Core\Database\Query\QueryHelper;
33
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
34
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
35
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
36
use TYPO3\CMS\Core\Html\HtmlParser;
37
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
38
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
39
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
40
use TYPO3\CMS\Core\LinkHandling\LinkService;
41
use TYPO3\CMS\Core\Log\LogManager;
42
use TYPO3\CMS\Core\Mail\MailMessage;
43
use TYPO3\CMS\Core\Page\AssetCollector;
44
use TYPO3\CMS\Core\Resource\Exception;
45
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
46
use TYPO3\CMS\Core\Resource\File;
47
use TYPO3\CMS\Core\Resource\FileInterface;
48
use TYPO3\CMS\Core\Resource\FileReference;
49
use TYPO3\CMS\Core\Resource\ProcessedFile;
50
use TYPO3\CMS\Core\Resource\ResourceFactory;
51
use TYPO3\CMS\Core\Service\DependencyOrderingService;
52
use TYPO3\CMS\Core\Service\FlexFormService;
53
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
54
use TYPO3\CMS\Core\Site\SiteFinder;
55
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
56
use TYPO3\CMS\Core\Type\BitSet;
57
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
58
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
59
use TYPO3\CMS\Core\Utility\ArrayUtility;
60
use TYPO3\CMS\Core\Utility\DebugUtility;
61
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
62
use TYPO3\CMS\Core\Utility\GeneralUtility;
63
use TYPO3\CMS\Core\Utility\HttpUtility;
64
use TYPO3\CMS\Core\Utility\MailUtility;
65
use TYPO3\CMS\Core\Utility\MathUtility;
66
use TYPO3\CMS\Core\Utility\StringUtility;
67
use TYPO3\CMS\Core\Versioning\VersionState;
68
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
69
use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
70
use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
71
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
72
use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
73
use TYPO3\CMS\Frontend\Imaging\GifBuilder;
74
use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
75
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
76
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
77
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
78
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
79
80
/**
81
 * This class contains all main TypoScript features.
82
 * This includes the rendering of TypoScript content objects (cObjects).
83
 * Is the backbone of TypoScript Template rendering.
84
 *
85
 * There are lots of functions you can use from your include-scripts.
86
 * The class is normally instantiated and referred to as "cObj".
87
 * When you call your own PHP-code typically through a USER or USER_INT cObject then it is this class that instantiates the object and calls the main method. Before it does so it will set (if you are using classes) a reference to itself in the internal variable "cObj" of the object. Thus you can access all functions and data from this class by $this->cObj->... from within you classes written to be USER or USER_INT content objects.
88
 */
89
class ContentObjectRenderer implements LoggerAwareInterface
90
{
91
    use LoggerAwareTrait;
92
93
    /**
94
     * @var ContainerInterface
95
     */
96
    protected $container;
97
98
    /**
99
     * @var array
100
     */
101
    public $align = [
102
        'center',
103
        'right',
104
        'left'
105
    ];
106
107
    /**
108
     * stdWrap functions in their correct order
109
     *
110
     * @see stdWrap()
111
     * @var string[]
112
     */
113
    public $stdWrapOrder = [
114
        'stdWrapPreProcess' => 'hook',
115
        // this is a placeholder for the first Hook
116
        'cacheRead' => 'hook',
117
        // this is a placeholder for checking if the content is available in cache
118
        'setContentToCurrent' => 'boolean',
119
        'setContentToCurrent.' => 'array',
120
        'addPageCacheTags' => 'string',
121
        'addPageCacheTags.' => 'array',
122
        'setCurrent' => 'string',
123
        'setCurrent.' => 'array',
124
        'lang.' => 'array',
125
        'data' => 'getText',
126
        'data.' => 'array',
127
        'field' => 'fieldName',
128
        'field.' => 'array',
129
        'current' => 'boolean',
130
        'current.' => 'array',
131
        'cObject' => 'cObject',
132
        'cObject.' => 'array',
133
        'numRows.' => 'array',
134
        'preUserFunc' => 'functionName',
135
        'stdWrapOverride' => 'hook',
136
        // this is a placeholder for the second Hook
137
        'override' => 'string',
138
        'override.' => 'array',
139
        'preIfEmptyListNum' => 'listNum',
140
        'preIfEmptyListNum.' => 'array',
141
        'ifNull' => 'string',
142
        'ifNull.' => 'array',
143
        'ifEmpty' => 'string',
144
        'ifEmpty.' => 'array',
145
        'ifBlank' => 'string',
146
        'ifBlank.' => 'array',
147
        'listNum' => 'listNum',
148
        'listNum.' => 'array',
149
        'trim' => 'boolean',
150
        'trim.' => 'array',
151
        'strPad.' => 'array',
152
        'stdWrap' => 'stdWrap',
153
        'stdWrap.' => 'array',
154
        'stdWrapProcess' => 'hook',
155
        // this is a placeholder for the third Hook
156
        'required' => 'boolean',
157
        'required.' => 'array',
158
        'if.' => 'array',
159
        'fieldRequired' => 'fieldName',
160
        'fieldRequired.' => 'array',
161
        'csConv' => 'string',
162
        'csConv.' => 'array',
163
        'parseFunc' => 'objectpath',
164
        'parseFunc.' => 'array',
165
        'HTMLparser' => 'boolean',
166
        'HTMLparser.' => 'array',
167
        'split.' => 'array',
168
        'replacement.' => 'array',
169
        'prioriCalc' => 'boolean',
170
        'prioriCalc.' => 'array',
171
        'char' => 'integer',
172
        'char.' => 'array',
173
        'intval' => 'boolean',
174
        'intval.' => 'array',
175
        'hash' => 'string',
176
        'hash.' => 'array',
177
        'round' => 'boolean',
178
        'round.' => 'array',
179
        'numberFormat.' => 'array',
180
        'expandList' => 'boolean',
181
        'expandList.' => 'array',
182
        'date' => 'dateconf',
183
        'date.' => 'array',
184
        'strtotime' => 'strtotimeconf',
185
        'strtotime.' => 'array',
186
        'strftime' => 'strftimeconf',
187
        'strftime.' => 'array',
188
        'age' => 'boolean',
189
        'age.' => 'array',
190
        'case' => 'case',
191
        'case.' => 'array',
192
        'bytes' => 'boolean',
193
        'bytes.' => 'array',
194
        'substring' => 'parameters',
195
        'substring.' => 'array',
196
        'cropHTML' => 'crop',
197
        'cropHTML.' => 'array',
198
        'stripHtml' => 'boolean',
199
        'stripHtml.' => 'array',
200
        'crop' => 'crop',
201
        'crop.' => 'array',
202
        'rawUrlEncode' => 'boolean',
203
        'rawUrlEncode.' => 'array',
204
        'htmlSpecialChars' => 'boolean',
205
        'htmlSpecialChars.' => 'array',
206
        'encodeForJavaScriptValue' => 'boolean',
207
        'encodeForJavaScriptValue.' => 'array',
208
        'doubleBrTag' => 'string',
209
        'doubleBrTag.' => 'array',
210
        'br' => 'boolean',
211
        'br.' => 'array',
212
        'brTag' => 'string',
213
        'brTag.' => 'array',
214
        'encapsLines.' => 'array',
215
        'keywords' => 'boolean',
216
        'keywords.' => 'array',
217
        'innerWrap' => 'wrap',
218
        'innerWrap.' => 'array',
219
        'innerWrap2' => 'wrap',
220
        'innerWrap2.' => 'array',
221
        'preCObject' => 'cObject',
222
        'preCObject.' => 'array',
223
        'postCObject' => 'cObject',
224
        'postCObject.' => 'array',
225
        'wrapAlign' => 'align',
226
        'wrapAlign.' => 'array',
227
        'typolink.' => 'array',
228
        'wrap' => 'wrap',
229
        'wrap.' => 'array',
230
        'noTrimWrap' => 'wrap',
231
        'noTrimWrap.' => 'array',
232
        'wrap2' => 'wrap',
233
        'wrap2.' => 'array',
234
        'dataWrap' => 'dataWrap',
235
        'dataWrap.' => 'array',
236
        'prepend' => 'cObject',
237
        'prepend.' => 'array',
238
        'append' => 'cObject',
239
        'append.' => 'array',
240
        'wrap3' => 'wrap',
241
        'wrap3.' => 'array',
242
        'orderedStdWrap' => 'stdWrap',
243
        'orderedStdWrap.' => 'array',
244
        'outerWrap' => 'wrap',
245
        'outerWrap.' => 'array',
246
        'insertData' => 'boolean',
247
        'insertData.' => 'array',
248
        'postUserFunc' => 'functionName',
249
        'postUserFuncInt' => 'functionName',
250
        'prefixComment' => 'string',
251
        'prefixComment.' => 'array',
252
        'editIcons' => 'string',
253
        'editIcons.' => 'array',
254
        'editPanel' => 'boolean',
255
        'editPanel.' => 'array',
256
        'cacheStore' => 'hook',
257
        // this is a placeholder for storing the content in cache
258
        'stdWrapPostProcess' => 'hook',
259
        // this is a placeholder for the last Hook
260
        'debug' => 'boolean',
261
        'debug.' => 'array',
262
        'debugFunc' => 'boolean',
263
        'debugFunc.' => 'array',
264
        'debugData' => 'boolean',
265
        'debugData.' => 'array'
266
    ];
267
268
    /**
269
     * Class names for accordant content object names
270
     *
271
     * @var array
272
     */
273
    protected $contentObjectClassMap = [];
274
275
    /**
276
     * Loaded with the current data-record.
277
     *
278
     * If the instance of this class is used to render records from the database those records are found in this array.
279
     * The function stdWrap has TypoScript properties that fetch field-data from this array.
280
     *
281
     * @var array
282
     * @see start()
283
     */
284
    public $data = [];
285
286
    /**
287
     * @var string
288
     */
289
    protected $table = '';
290
291
    /**
292
     * Used for backup
293
     *
294
     * @var array
295
     */
296
    public $oldData = [];
297
298
    /**
299
     * If this is set with an array before stdWrap, it's used instead of $this->data in the data-property in stdWrap
300
     *
301
     * @var string
302
     */
303
    public $alternativeData = '';
304
305
    /**
306
     * Used by the parseFunc function and is loaded with tag-parameters when parsing tags.
307
     *
308
     * @var array
309
     */
310
    public $parameters = [];
311
312
    /**
313
     * @var string
314
     */
315
    public $currentValKey = 'currentValue_kidjls9dksoje';
316
317
    /**
318
     * This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation.
319
     * Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered.
320
     *
321
     * @var string
322
     */
323
    public $currentRecord = '';
324
325
    /**
326
     * Set in RecordsContentObject and ContentContentObject to the current number of records selected in a query.
327
     *
328
     * @var int
329
     */
330
    public $currentRecordTotal = 0;
331
332
    /**
333
     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
334
     *
335
     * @var int
336
     */
337
    public $currentRecordNumber = 0;
338
339
    /**
340
     * Incremented in RecordsContentObject and ContentContentObject before each record rendering.
341
     *
342
     * @var int
343
     */
344
    public $parentRecordNumber = 0;
345
346
    /**
347
     * If the ContentObjectRender was started from ContentContentObject, RecordsContentObject or SearchResultContentObject this array has two keys, 'data' and 'currentRecord' which indicates the record and data for the parent cObj.
348
     *
349
     * @var array
350
     */
351
    public $parentRecord = [];
352
353
    /**
354
     * 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.
355
     *
356
     * @var array
357
     */
358
    public $checkPid_cache = [];
359
360
    /**
361
     * @var string|int
362
     */
363
    public $checkPid_badDoktypeList = PageRepository::DOKTYPE_RECYCLER;
364
365
    /**
366
     * This will be set by typoLink() to the url of the most recent link created.
367
     *
368
     * @var string
369
     */
370
    public $lastTypoLinkUrl = '';
371
372
    /**
373
     * DO. link target.
374
     *
375
     * @var string
376
     */
377
    public $lastTypoLinkTarget = '';
378
379
    /**
380
     * @var array
381
     */
382
    public $lastTypoLinkLD = [];
383
384
    /**
385
     * array that registers rendered content elements (or any table) to make sure they are not rendered recursively!
386
     *
387
     * @var array
388
     */
389
    public $recordRegister = [];
390
391
    /**
392
     * Additionally registered content object types and class names
393
     *
394
     * @var array
395
     * @deprecated - will be removed in TYPO3 v11.0
396
     */
397
    protected $cObjHookObjectsRegistry = [];
398
399
    /**
400
     * @var array
401
     * @deprecated - will be removed in TYPO3 v11.0
402
     */
403
    public $cObjHookObjectsArr = [];
404
405
    /**
406
     * Containing hook objects for stdWrap
407
     *
408
     * @var array
409
     */
410
    protected $stdWrapHookObjects = [];
411
412
    /**
413
     * Containing hook objects for getImgResource
414
     *
415
     * @var array
416
     */
417
    protected $getImgResourceHookObjects;
418
419
    /**
420
     * @var File Current file objects (during iterations over files)
421
     */
422
    protected $currentFile;
423
424
    /**
425
     * Set to TRUE by doConvertToUserIntObject() if USER object wants to become USER_INT
426
     * @var bool
427
     */
428
    public $doConvertToUserIntObject = false;
429
430
    /**
431
     * Indicates current object type. Can hold one of OBJECTTYPE_ constants or FALSE.
432
     * The value is set and reset inside USER() function. Any time outside of
433
     * USER() it is FALSE.
434
     * @var bool
435
     */
436
    protected $userObjectType = false;
437
438
    /**
439
     * @var array
440
     */
441
    protected $stopRendering = [];
442
443
    /**
444
     * @var int
445
     */
446
    protected $stdWrapRecursionLevel = 0;
447
448
    /**
449
     * @var TypoScriptFrontendController
450
     */
451
    protected $typoScriptFrontendController;
452
453
    /**
454
     * Indicates that object type is USER.
455
     *
456
     * @see ContentObjectRender::$userObjectType
457
     */
458
    const OBJECTTYPE_USER_INT = 1;
459
    /**
460
     * Indicates that object type is USER.
461
     *
462
     * @see ContentObjectRender::$userObjectType
463
     */
464
    const OBJECTTYPE_USER = 2;
465
466
    /**
467
     * @param TypoScriptFrontendController $typoScriptFrontendController
468
     * @param ContainerInterface $container
469
     */
470
    public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null, ContainerInterface $container = null)
471
    {
472
        $this->typoScriptFrontendController = $typoScriptFrontendController;
473
        $this->contentObjectClassMap = $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'];
474
        $this->container = $container;
475
    }
476
477
    /**
478
     * Prevent several objects from being serialized.
479
     * If currentFile is set, it is either a File or a FileReference object. As the object itself can't be serialized,
480
     * we have store a hash and restore the object in __wakeup()
481
     *
482
     * @return array
483
     */
484
    public function __sleep()
485
    {
486
        $vars = get_object_vars($this);
487
        unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container']);
488
        if ($this->currentFile instanceof FileReference) {
0 ignored issues
show
introduced by
$this->currentFile is never a sub-type of TYPO3\CMS\Core\Resource\FileReference.
Loading history...
489
            $this->currentFile = 'FileReference:' . $this->currentFile->getUid();
490
        } elseif ($this->currentFile instanceof File) {
0 ignored issues
show
introduced by
$this->currentFile is always a sub-type of TYPO3\CMS\Core\Resource\File.
Loading history...
491
            $this->currentFile = 'File:' . $this->currentFile->getIdentifier();
0 ignored issues
show
Documentation Bug introduced by
It seems like 'File:' . $this->currentFile->getIdentifier() of type string is incompatible with the declared type TYPO3\CMS\Core\Resource\File of property $currentFile.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
492
        } else {
493
            unset($vars['currentFile']);
494
        }
495
        return array_keys($vars);
496
    }
497
498
    /**
499
     * Restore currentFile from hash.
500
     * If currentFile references a File, the identifier equals file identifier.
501
     * If it references a FileReference the identifier equals the uid of the reference.
502
     */
503
    public function __wakeup()
504
    {
505
        if (isset($GLOBALS['TSFE'])) {
506
            $this->typoScriptFrontendController = $GLOBALS['TSFE'];
507
        }
508
        if ($this->currentFile !== null && is_string($this->currentFile)) {
0 ignored issues
show
introduced by
The condition is_string($this->currentFile) is always false.
Loading history...
509
            [$objectType, $identifier] = explode(':', $this->currentFile, 2);
510
            try {
511
                if ($objectType === 'File') {
512
                    $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($identifier);
513
                } elseif ($objectType === 'FileReference') {
514
                    $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject($identifier);
515
                }
516
            } catch (ResourceDoesNotExistException $e) {
517
                $this->currentFile = null;
518
            }
519
        }
520
        $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
521
        $this->container = GeneralUtility::getContainer();
522
    }
523
524
    /**
525
     * Allow injecting content object class map.
526
     *
527
     * This method is private API, please use configuration
528
     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
529
     *
530
     * @internal
531
     * @param array $contentObjectClassMap
532
     */
533
    public function setContentObjectClassMap(array $contentObjectClassMap)
534
    {
535
        $this->contentObjectClassMap = $contentObjectClassMap;
536
    }
537
538
    /**
539
     * Register a single content object name to class name
540
     *
541
     * This method is private API, please use configuration
542
     * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects
543
     *
544
     * @param string $className
545
     * @param string $contentObjectName
546
     * @internal
547
     */
548
    public function registerContentObjectClass($className, $contentObjectName)
549
    {
550
        $this->contentObjectClassMap[$contentObjectName] = $className;
551
    }
552
553
    /**
554
     * Class constructor.
555
     * Well, it has to be called manually since it is not a real constructor function.
556
     * 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.
557
     *
558
     * @param array $data The record data that is rendered.
559
     * @param string $table The table that the data record is from.
560
     */
561
    public function start($data, $table = '')
562
    {
563
        $this->data = $data;
564
        $this->table = $table;
565
        $this->currentRecord = $table !== ''
566
            ? $table . ':' . ($this->data['uid'] ?? '')
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
567
            : '';
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
568
        $this->parameters = [];
569
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClass'] ?? [] as $classArr) {
570
            trigger_error('The hook $TYPO3_CONF_VARS[SC_OPTIONS][tslib/class.tslib_content.php][cObjTypeAndClass] for adding custom cObjects will be removed in TYPO3 v11.0. Use a custom cObject which you can register directly instead.', E_USER_DEPRECATED);
571
            $this->cObjHookObjectsRegistry[$classArr[0]] = $classArr[1];
572
        }
573
        $this->stdWrapHookObjects = [];
574
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] ?? [] as $className) {
575
            $hookObject = GeneralUtility::makeInstance($className);
576
            if (!$hookObject instanceof ContentObjectStdWrapHookInterface) {
577
                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectStdWrapHookInterface::class, 1195043965);
578
            }
579
            $this->stdWrapHookObjects[] = $hookObject;
580
        }
581
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] ?? [] as $className) {
582
            $postInitializationProcessor = GeneralUtility::makeInstance($className);
583
            if (!$postInitializationProcessor instanceof ContentObjectPostInitHookInterface) {
584
                throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectPostInitHookInterface::class, 1274563549);
585
            }
586
            $postInitializationProcessor->postProcessContentObjectInitialization($this);
587
        }
588
    }
589
590
    /**
591
     * Returns the current table
592
     *
593
     * @return string
594
     */
595
    public function getCurrentTable()
596
    {
597
        return $this->table;
598
    }
599
600
    /**
601
     * Gets the 'getImgResource' hook objects.
602
     * The first call initializes the accordant objects.
603
     *
604
     * @return array The 'getImgResource' hook objects (if any)
605
     */
606
    protected function getGetImgResourceHookObjects()
607
    {
608
        if (!isset($this->getImgResourceHookObjects)) {
609
            $this->getImgResourceHookObjects = [];
610
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] ?? [] as $className) {
611
                $hookObject = GeneralUtility::makeInstance($className);
612
                if (!$hookObject instanceof ContentObjectGetImageResourceHookInterface) {
613
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetImageResourceHookInterface::class, 1218636383);
614
                }
615
                $this->getImgResourceHookObjects[] = $hookObject;
616
            }
617
        }
618
        return $this->getImgResourceHookObjects;
619
    }
620
621
    /**
622
     * Sets the internal variable parentRecord with information about current record.
623
     * 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.
624
     *
625
     * @param array $data The record array
626
     * @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.
627
     * @internal
628
     */
629
    public function setParent($data, $currentRecord)
630
    {
631
        $this->parentRecord = [
632
            'data' => $data,
633
            'currentRecord' => $currentRecord
634
        ];
635
    }
636
637
    /***********************************************
638
     *
639
     * CONTENT_OBJ:
640
     *
641
     ***********************************************/
642
    /**
643
     * Returns the "current" value.
644
     * 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.
645
     * It's like "load accumulator" in the good old C64 days... basically a "register" you can use as you like.
646
     * 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.
647
     *
648
     * @return mixed The "current" value
649
     */
650
    public function getCurrentVal()
651
    {
652
        return $this->data[$this->currentValKey];
653
    }
654
655
    /**
656
     * Sets the "current" value.
657
     *
658
     * @param mixed $value The variable that you want to set as "current
659
     * @see getCurrentVal()
660
     */
661
    public function setCurrentVal($value)
662
    {
663
        $this->data[$this->currentValKey] = $value;
664
    }
665
666
    /**
667
     * Rendering of a "numerical array" of cObjects from TypoScript
668
     * Will call ->cObjGetSingle() for each cObject found and accumulate the output.
669
     *
670
     * @param array $setup array with cObjects as values.
671
     * @param string $addKey A prefix for the debugging information
672
     * @return string Rendered output from the cObjects in the array.
673
     * @see cObjGetSingle()
674
     */
675
    public function cObjGet($setup, $addKey = '')
676
    {
677
        if (!is_array($setup)) {
0 ignored issues
show
introduced by
The condition is_array($setup) is always true.
Loading history...
678
            return '';
679
        }
680
        $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($setup);
681
        $content = '';
682
        foreach ($sKeyArray as $theKey) {
683
            $theValue = $setup[$theKey];
684
            if ((int)$theKey && strpos($theKey, '.') === false) {
685
                $conf = $setup[$theKey . '.'];
686
                $content .= $this->cObjGetSingle($theValue, $conf, $addKey . $theKey);
687
            }
688
        }
689
        return $content;
690
    }
691
692
    /**
693
     * Renders a content object
694
     *
695
     * @param string $name The content object name, eg. "TEXT" or "USER" or "IMAGE"
696
     * @param array $conf The array with TypoScript properties for the content object
697
     * @param string $TSkey A string label used for the internal debugging tracking.
698
     * @return string cObject output
699
     * @throws \UnexpectedValueException
700
     */
701
    public function cObjGetSingle($name, $conf, $TSkey = '__')
702
    {
703
        $content = '';
704
        // Checking that the function is not called eternally. This is done by interrupting at a depth of 100
705
        $this->getTypoScriptFrontendController()->cObjectDepthCounter--;
706
        if ($this->getTypoScriptFrontendController()->cObjectDepthCounter > 0) {
707
            $timeTracker = $this->getTimeTracker();
708
            $name = trim($name);
709
            if ($timeTracker->LR) {
710
                $timeTracker->push($TSkey, $name);
711
            }
712
            // Checking if the COBJ is a reference to another object. (eg. name of 'some.object =< styles.something')
713
            if (isset($name[0]) && $name[0] === '<') {
714
                $key = trim(substr($name, 1));
715
                $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
716
                // $name and $conf is loaded with the referenced values.
717
                $confOverride = is_array($conf) ? $conf : [];
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
718
                [$name, $conf] = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
719
                $conf = array_replace_recursive(is_array($conf) ? $conf : [], $confOverride);
720
                // Getting the cObject
721
                $timeTracker->incStackPointer();
722
                $content .= $this->cObjGetSingle($name, $conf, $key);
723
                $timeTracker->decStackPointer();
724
            } else {
725
                $hooked = false;
726
                // Application defined cObjects
727
                // @deprecated since TYPO3 v10.4 - will be removed in TYPO3 v11.0
728
                if (!empty($this->cObjHookObjectsRegistry[$name])) {
729
                    if (empty($this->cObjHookObjectsArr[$name])) {
730
                        $this->cObjHookObjectsArr[$name] = GeneralUtility::makeInstance($this->cObjHookObjectsRegistry[$name]);
731
                    }
732
                    $hookObj = $this->cObjHookObjectsArr[$name];
733
                    if (method_exists($hookObj, 'cObjGetSingleExt')) {
734
                        $content .= $hookObj->cObjGetSingleExt($name, $conf, $TSkey, $this);
735
                        $hooked = true;
736
                    }
737
                }
738
                if (!$hooked) {
739
                    $contentObject = $this->getContentObject($name);
740
                    if ($contentObject) {
741
                        $content .= $this->render($contentObject, $conf);
742
                    } else {
743
                        // Call hook functions for extra processing
744
                        if ($name) {
745
                            if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClassDefault'])) {
746
                                trigger_error('The hook $TYPO3_CONF_VARS[SC_OPTIONS][tslib/class.tslib_content.php][cObjTypeAndClassDefault] for adding custom processing will be removed. Use a custom cObject which you can register directly instead.', E_USER_DEPRECATED);
747
                            }
748
                            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClassDefault'] ?? [] as $className) {
749
                                $hookObject = GeneralUtility::makeInstance($className);
750
                                if (!$hookObject instanceof ContentObjectGetSingleHookInterface) {
751
                                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetSingleHookInterface::class, 1195043731);
752
                                }
753
                                /** @var ContentObjectGetSingleHookInterface $hookObject */
754
                                $content .= $hookObject->getSingleContentObject($name, (array)$conf, $TSkey, $this);
755
                            }
756
                        } else {
757
                            // Log error in AdminPanel
758
                            $warning = sprintf('Content Object "%s" does not exist', $name);
759
                            $timeTracker->setTSlogMessage($warning, 2);
760
                        }
761
                    }
762
                }
763
            }
764
            if ($timeTracker->LR) {
765
                $timeTracker->pull($content);
766
            }
767
        }
768
        // Increasing on exit...
769
        $this->getTypoScriptFrontendController()->cObjectDepthCounter++;
770
        return $content;
771
    }
772
773
    /**
774
     * Returns a new content object of type $name.
775
     * This content object needs to be registered as content object
776
     * in $this->contentObjectClassMap
777
     *
778
     * @param string $name
779
     * @return AbstractContentObject|null
780
     * @throws ContentRenderingException
781
     */
782
    public function getContentObject($name)
783
    {
784
        if (!isset($this->contentObjectClassMap[$name])) {
785
            return null;
786
        }
787
        $fullyQualifiedClassName = $this->contentObjectClassMap[$name];
788
        $contentObject = GeneralUtility::makeInstance($fullyQualifiedClassName, $this);
789
        if (!($contentObject instanceof AbstractContentObject)) {
790
            throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295);
791
        }
792
        return $contentObject;
793
    }
794
795
    /********************************************
796
     *
797
     * Functions rendering content objects (cObjects)
798
     *
799
     ********************************************/
800
801
    /**
802
     * Renders a content object by taking exception and cache handling
803
     * into consideration
804
     *
805
     * @param AbstractContentObject $contentObject Content object instance
806
     * @param array $configuration Array of TypoScript properties
807
     *
808
     * @throws ContentRenderingException
809
     * @throws \Exception
810
     * @return string
811
     */
812
    public function render(AbstractContentObject $contentObject, $configuration = [])
813
    {
814
        $content = '';
815
816
        // Evaluate possible cache and return
817
        $cacheConfiguration = $configuration['cache.'] ?? null;
818
        if ($cacheConfiguration !== null) {
819
            unset($configuration['cache.']);
820
            $cache = $this->getFromCache($cacheConfiguration);
821
            if ($cache !== false) {
822
                return $cache;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $cache also could return the type true which is incompatible with the documented return type string.
Loading history...
823
            }
824
        }
825
826
        // Render content
827
        try {
828
            $content .= $contentObject->render($configuration);
829
        } catch (ContentRenderingException $exception) {
830
            // Content rendering Exceptions indicate a critical problem which should not be
831
            // caught e.g. when something went wrong with Exception handling itself
832
            throw $exception;
833
        } catch (\Exception $exception) {
834
            $exceptionHandler = $this->createExceptionHandler($configuration);
835
            if ($exceptionHandler === null) {
836
                throw $exception;
837
            }
838
            $content = $exceptionHandler->handle($exception, $contentObject, $configuration);
839
        }
840
841
        // Store cache
842
        if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) {
843
            $key = $this->calculateCacheKey($cacheConfiguration);
844
            if (!empty($key)) {
845
                /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
846
                $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
847
                $tags = $this->calculateCacheTags($cacheConfiguration);
848
                $lifetime = $this->calculateCacheLifetime($cacheConfiguration);
849
                $cacheFrontend->set($key, $content, $tags, $lifetime);
850
            }
851
        }
852
853
        return $content;
854
    }
855
856
    /**
857
     * Creates the content object exception handler from local content object configuration
858
     * or, from global configuration if not explicitly disabled in local configuration
859
     *
860
     * @param array $configuration
861
     * @return ExceptionHandlerInterface|null
862
     * @throws ContentRenderingException
863
     */
864
    protected function createExceptionHandler($configuration = [])
865
    {
866
        $exceptionHandler = null;
867
        $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration);
868
        if (!empty($exceptionHandlerClassName)) {
869
            $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName, $this->mergeExceptionHandlerConfiguration($configuration));
870
            if (!$exceptionHandler instanceof ExceptionHandlerInterface) {
871
                throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369);
872
            }
873
        }
874
875
        return $exceptionHandler;
876
    }
877
878
    /**
879
     * Determine exception handler class name from global and content object configuration
880
     *
881
     * @param array $configuration
882
     * @return string|null
883
     */
884
    protected function determineExceptionHandlerClassName($configuration)
885
    {
886
        $exceptionHandlerClassName = null;
887
        $tsfe = $this->getTypoScriptFrontendController();
888
        if (!isset($tsfe->config['config']['contentObjectExceptionHandler'])) {
889
            if (Environment::getContext()->isProduction()) {
890
                $exceptionHandlerClassName = '1';
891
            }
892
        } else {
893
            $exceptionHandlerClassName = $tsfe->config['config']['contentObjectExceptionHandler'];
894
        }
895
896
        if (isset($configuration['exceptionHandler'])) {
897
            $exceptionHandlerClassName = $configuration['exceptionHandler'];
898
        }
899
900
        if ($exceptionHandlerClassName === '1') {
901
            $exceptionHandlerClassName = ProductionExceptionHandler::class;
902
        }
903
904
        return $exceptionHandlerClassName;
905
    }
906
907
    /**
908
     * Merges global exception handler configuration with the one from the content object
909
     * and returns the merged exception handler configuration
910
     *
911
     * @param array $configuration
912
     * @return array
913
     */
914
    protected function mergeExceptionHandlerConfiguration($configuration)
915
    {
916
        $exceptionHandlerConfiguration = [];
917
        $tsfe = $this->getTypoScriptFrontendController();
918
        if (!empty($tsfe->config['config']['contentObjectExceptionHandler.'])) {
919
            $exceptionHandlerConfiguration = $tsfe->config['config']['contentObjectExceptionHandler.'];
920
        }
921
        if (!empty($configuration['exceptionHandler.'])) {
922
            $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']);
923
        }
924
925
        return $exceptionHandlerConfiguration;
926
    }
927
928
    /**
929
     * Retrieves a type of object called as USER or USER_INT. Object can detect their
930
     * type by using this call. It returns OBJECTTYPE_USER_INT or OBJECTTYPE_USER depending on the
931
     * current object execution. In all other cases it will return FALSE to indicate
932
     * a call out of context.
933
     *
934
     * @return mixed One of OBJECTTYPE_ class constants or FALSE
935
     */
936
    public function getUserObjectType()
937
    {
938
        return $this->userObjectType;
939
    }
940
941
    /**
942
     * Sets the user object type
943
     *
944
     * @param mixed $userObjectType
945
     */
946
    public function setUserObjectType($userObjectType)
947
    {
948
        $this->userObjectType = $userObjectType;
949
    }
950
951
    /**
952
     * Requests the current USER object to be converted to USER_INT.
953
     */
954
    public function convertToUserIntObject()
955
    {
956
        if ($this->userObjectType !== self::OBJECTTYPE_USER) {
0 ignored issues
show
introduced by
The condition $this->userObjectType !== self::OBJECTTYPE_USER is always true.
Loading history...
957
            $this->getTimeTracker()->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', 2);
958
        } else {
959
            $this->doConvertToUserIntObject = true;
960
        }
961
    }
962
963
    /************************************
964
     *
965
     * Various helper functions for content objects:
966
     *
967
     ************************************/
968
    /**
969
     * Converts a given config in Flexform to a conf-array
970
     *
971
     * @param string|array $flexData Flexform data
972
     * @param array $conf Array to write the data into, by reference
973
     * @param bool $recursive Is set if called recursive. Don't call function with this parameter, it's used inside the function only
974
     */
975
    public function readFlexformIntoConf($flexData, &$conf, $recursive = false)
976
    {
977
        if ($recursive === false && is_string($flexData)) {
978
            $flexData = GeneralUtility::xml2array($flexData, 'T3');
979
        }
980
        if (is_array($flexData) && isset($flexData['data']['sDEF']['lDEF'])) {
981
            $flexData = $flexData['data']['sDEF']['lDEF'];
982
        }
983
        if (!is_array($flexData)) {
984
            return;
985
        }
986
        foreach ($flexData as $key => $value) {
987
            if (!is_array($value)) {
988
                continue;
989
            }
990
            if (isset($value['el'])) {
991
                if (is_array($value['el']) && !empty($value['el'])) {
992
                    foreach ($value['el'] as $ekey => $element) {
993
                        if (isset($element['vDEF'])) {
994
                            $conf[$ekey] = $element['vDEF'];
995
                        } else {
996
                            if (is_array($element)) {
997
                                $this->readFlexformIntoConf($element, $conf[$key][key($element)][$ekey], true);
998
                            } else {
999
                                $this->readFlexformIntoConf($element, $conf[$key][$ekey], true);
1000
                            }
1001
                        }
1002
                    }
1003
                } else {
1004
                    $this->readFlexformIntoConf($value['el'], $conf[$key], true);
1005
                }
1006
            }
1007
            if (isset($value['vDEF'])) {
1008
                $conf[$key] = $value['vDEF'];
1009
            }
1010
        }
1011
    }
1012
1013
    /**
1014
     * Returns all parents of the given PID (Page UID) list
1015
     *
1016
     * @param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap
1017
     * @param array $pidConf stdWrap array for the list
1018
     * @return string A list of PIDs
1019
     * @internal
1020
     */
1021
    public function getSlidePids($pidList, $pidConf)
1022
    {
1023
        // todo: phpstan states that $pidConf always exists and is not nullable. At the moment, this is a false positive
1024
        //       as null can be passed into this method via $pidConf. As soon as more strict types are used, this isset
1025
        //       check must be replaced with a more appropriate check like empty or count.
1026
        $pidList = isset($pidConf) ? trim($this->stdWrap($pidList, $pidConf)) : trim($pidList);
1027
        if ($pidList === '') {
1028
            $pidList = 'this';
1029
        }
1030
        $tsfe = $this->getTypoScriptFrontendController();
1031
        $listArr = null;
1032
        if (trim($pidList)) {
1033
            $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $pidList));
1034
            $listArr = $this->checkPidArray($listArr);
1035
        }
1036
        $pidList = [];
1037
        if (is_array($listArr) && !empty($listArr)) {
1038
            foreach ($listArr as $uid) {
1039
                $page = $tsfe->sys_page->getPage($uid);
1040
                if (!$page['is_siteroot']) {
1041
                    $pidList[] = $page['pid'];
1042
                }
1043
            }
1044
        }
1045
        return implode(',', $pidList);
1046
    }
1047
1048
    /**
1049
     * Returns a <img> tag with the image file defined by $file and processed according to the properties in the TypoScript array.
1050
     * Mostly this function is a sub-function to the IMAGE function which renders the IMAGE cObject in TypoScript.
1051
     * This function is called by "$this->cImage($conf['file'], $conf);" from IMAGE().
1052
     *
1053
     * @param string $file File TypoScript resource
1054
     * @param array $conf TypoScript configuration properties
1055
     * @return string HTML <img> tag, (possibly wrapped in links and other HTML) if any image found.
1056
     * @deprecated will be removed in TYPO3 v11.0
1057
     * @see IMAGE()
1058
     */
1059
    public function cImage($file, $conf)
1060
    {
1061
        trigger_error('cObj->cImage() will be removed in TYPO3 v11.0. This functionality is integrated into ImageContentObject now.', E_USER_DEPRECATED);
1062
        $tsfe = $this->getTypoScriptFrontendController();
1063
        $info = $this->getImgResource($file, $conf['file.']);
1064
        $tsfe->lastImageInfo = $info;
0 ignored issues
show
Bug Best Practice introduced by
The property $lastImageInfo is declared private in TYPO3\CMS\Frontend\Contr...criptFrontendController. Since you implement __set, consider adding a @property or @property-write.
Loading history...
1065
        if (!is_array($info)) {
1066
            return '';
1067
        }
1068
        if (is_file(Environment::getPublicPath() . '/' . $info['3'])) {
1069
            $source = $tsfe->absRefPrefix . str_replace('%2F', '/', rawurlencode($info['3']));
1070
        } else {
1071
            $source = $info[3];
1072
        }
1073
        // Remove file objects for AssetCollector, as it only allows to store scalar values
1074
        unset($info['originalFile'], $info['processedFile']);
1075
        GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
1076
            $source,
1077
            $info
1078
        );
1079
1080
        $layoutKey = $this->stdWrap($conf['layoutKey'], $conf['layoutKey.']);
1081
        $imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf);
1082
        $sourceCollection = $this->getImageSourceCollection($layoutKey, $conf, $file);
1083
1084
        // This array is used to collect the image-refs on the page...
1085
        $tsfe->imagesOnPage[] = $source;
0 ignored issues
show
Bug Best Practice introduced by
The property $imagesOnPage is declared private in TYPO3\CMS\Frontend\Contr...criptFrontendController. Since you implement __set, consider adding a @property or @property-write.
Loading history...
1086
        $altParam = $this->getAltParam($conf);
1087
        $params = $this->stdWrapValue('params', $conf);
1088
        if ($params !== '' && $params[0] !== ' ') {
1089
            $params = ' ' . $params;
1090
        }
1091
1092
        $imageTagValues = [
1093
            'width' =>  (int)$info[0],
1094
            'height' => (int)$info[1],
1095
            'src' => htmlspecialchars($source),
1096
            'params' => $params,
1097
            'altParams' => $altParam,
1098
            'border' =>  $this->getBorderAttr(' border="' . (int)$conf['border'] . '"'),
1099
            'sourceCollection' => $sourceCollection,
1100
            'selfClosingTagSlash' => !empty($tsfe->xhtmlDoctype) ? ' /' : '',
1101
        ];
1102
1103
        $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1104
        $theValue = $markerTemplateEngine->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
1105
1106
        $linkWrap = isset($conf['linkWrap.']) ? $this->stdWrap($conf['linkWrap'], $conf['linkWrap.']) : $conf['linkWrap'];
1107
        if ($linkWrap) {
1108
            $theValue = $this->linkWrap($theValue, $linkWrap);
1109
        } elseif ($conf['imageLinkWrap']) {
1110
            $originalFile = !empty($info['originalFile']) ? $info['originalFile'] : $info['origFile'];
1111
            $theValue = $this->imageLinkWrap($theValue, $originalFile, $conf['imageLinkWrap.']);
1112
        }
1113
        $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
1114
        if ((string)$wrap !== '') {
1115
            $theValue = $this->wrap($theValue, $conf['wrap']);
1116
        }
1117
        return $theValue;
1118
    }
1119
1120
    /**
1121
     * Returns the 'border' attribute for an <img> tag only if the doctype is not xhtml_strict, xhtml_11 or html5
1122
     * or if the config parameter 'disableImgBorderAttr' is not set.
1123
     *
1124
     * @param string $borderAttr The border attribute
1125
     * @return string The border attribute
1126
     * @deprecated will be removed in TYPO3 v11.0.
1127
     */
1128
    public function getBorderAttr($borderAttr)
1129
    {
1130
        trigger_error('cObj->getBorderAttr() will be removed in TYPO3 v11.0. This functionality is integrated into ImageContentObject.', E_USER_DEPRECATED);
1131
        $tsfe = $this->getTypoScriptFrontendController();
1132
        $docType = $tsfe->xhtmlDoctype;
1133
        if (
1134
            $docType !== 'xhtml_strict' && $docType !== 'xhtml_11'
1135
            && $tsfe->config['config']['doctype'] !== 'html5'
1136
            && !$tsfe->config['config']['disableImgBorderAttr']
1137
        ) {
1138
            return $borderAttr;
1139
        }
1140
        return '';
1141
    }
1142
1143
    /**
1144
     * Returns the html-template for rendering the image-Tag if no template is defined via typoscript the
1145
     * default <img> tag template is returned
1146
     *
1147
     * @param string $layoutKey rendering key
1148
     * @param array $conf TypoScript configuration properties
1149
     * @return string
1150
     * @deprecated will be removed in TYPO3 v11.0.
1151
     */
1152
    public function getImageTagTemplate($layoutKey, $conf)
1153
    {
1154
        trigger_error('cObj->getImageTagTemplate() will be removed in TYPO3 v11.0. This functionality is integrated into ImageContentObject.', E_USER_DEPRECATED);
1155
        if ($layoutKey && isset($conf['layout.']) && isset($conf['layout.'][$layoutKey . '.'])) {
1156
            $imageTagLayout = $this->stdWrap(
1157
                $conf['layout.'][$layoutKey . '.']['element'] ?? '',
1158
                $conf['layout.'][$layoutKey . '.']['element.'] ?? []
1159
            );
1160
        } else {
1161
            $imageTagLayout = '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###BORDER######SELFCLOSINGTAGSLASH###>';
1162
        }
1163
        return $imageTagLayout;
1164
    }
1165
1166
    /**
1167
     * Render alternate sources for the image tag. If no source collection is given an empty string is returned.
1168
     *
1169
     * @param string $layoutKey rendering key
1170
     * @param array $conf TypoScript configuration properties
1171
     * @param string $file
1172
     * @throws \UnexpectedValueException
1173
     * @return string
1174
     * @deprecated will be removed in TYPO3 v11.0.
1175
     */
1176
    public function getImageSourceCollection($layoutKey, $conf, $file)
1177
    {
1178
        trigger_error('cObj->getImageSourceCollection() will be removed in TYPO3 v11.0. This functionality is integrated into ImageContentObject.', E_USER_DEPRECATED);
1179
        $sourceCollection = '';
1180
        if ($layoutKey
1181
            && isset($conf['sourceCollection.']) && $conf['sourceCollection.']
1182
            && (
1183
                isset($conf['layout.'][$layoutKey . '.']['source']) && $conf['layout.'][$layoutKey . '.']['source']
1184
                || isset($conf['layout.'][$layoutKey . '.']['source.']) && $conf['layout.'][$layoutKey . '.']['source.']
1185
            )
1186
        ) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
1187
1188
            // find active sourceCollection
1189
            $activeSourceCollections = [];
1190
            foreach ($conf['sourceCollection.'] as $sourceCollectionKey => $sourceCollectionConfiguration) {
1191
                if (substr($sourceCollectionKey, -1) === '.') {
1192
                    if (empty($sourceCollectionConfiguration['if.']) || $this->checkIf($sourceCollectionConfiguration['if.'])) {
1193
                        $activeSourceCollections[] = $sourceCollectionConfiguration;
1194
                    }
1195
                }
1196
            }
1197
1198
            // apply option split to configurations
1199
            $tsfe = $this->getTypoScriptFrontendController();
1200
            $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
1201
            $srcLayoutOptionSplitted = $typoScriptService->explodeConfigurationForOptionSplit((array)$conf['layout.'][$layoutKey . '.'], count($activeSourceCollections));
1202
1203
            // render sources
1204
            foreach ($activeSourceCollections as $key => $sourceConfiguration) {
1205
                $sourceLayout = $this->stdWrap(
1206
                    $srcLayoutOptionSplitted[$key]['source'] ?? '',
1207
                    $srcLayoutOptionSplitted[$key]['source.'] ?? []
1208
                );
1209
1210
                $sourceRenderConfiguration = [
1211
                    'file' => $file,
1212
                    'file.' => $conf['file.'] ?? null
1213
                ];
1214
1215
                if (isset($sourceConfiguration['quality']) || isset($sourceConfiguration['quality.'])) {
1216
                    $imageQuality = $sourceConfiguration['quality'] ?? '';
1217
                    if (isset($sourceConfiguration['quality.'])) {
1218
                        $imageQuality = $this->stdWrap($sourceConfiguration['quality'], $sourceConfiguration['quality.']);
1219
                    }
1220
                    if ($imageQuality) {
1221
                        $sourceRenderConfiguration['file.']['params'] = '-quality ' . (int)$imageQuality;
1222
                    }
1223
                }
1224
1225
                if (isset($sourceConfiguration['pixelDensity'])) {
1226
                    $pixelDensity = (int)$this->stdWrap(
1227
                        $sourceConfiguration['pixelDensity'] ?? '',
1228
                        $sourceConfiguration['pixelDensity.'] ?? []
1229
                    );
1230
                } else {
1231
                    $pixelDensity = 1;
1232
                }
1233
                $dimensionKeys = ['width', 'height', 'maxW', 'minW', 'maxH', 'minH', 'maxWidth', 'maxHeight', 'XY'];
1234
                foreach ($dimensionKeys as $dimensionKey) {
1235
                    $dimension = $this->stdWrap(
1236
                        $sourceConfiguration[$dimensionKey] ?? '',
1237
                        $sourceConfiguration[$dimensionKey . '.'] ?? []
1238
                    );
1239
                    if (!$dimension) {
1240
                        $dimension = $this->stdWrap(
1241
                            $conf['file.'][$dimensionKey] ?? '',
1242
                            $conf['file.'][$dimensionKey . '.'] ?? []
1243
                        );
1244
                    }
1245
                    if ($dimension) {
1246
                        if (strpos($dimension, 'c') !== false && ($dimensionKey === 'width' || $dimensionKey === 'height')) {
1247
                            $dimensionParts = explode('c', $dimension, 2);
1248
                            $dimension = ((int)$dimensionParts[0] * $pixelDensity) . 'c';
1249
                            if ($dimensionParts[1]) {
1250
                                $dimension .= $dimensionParts[1];
1251
                            }
1252
                        } elseif ($dimensionKey === 'XY') {
1253
                            $dimensionParts = GeneralUtility::intExplode(',', $dimension, false, 2);
1254
                            $dimension = $dimensionParts[0] * $pixelDensity;
1255
                            if ($dimensionParts[1]) {
1256
                                $dimension .= ',' . $dimensionParts[1] * $pixelDensity;
1257
                            }
1258
                        } else {
1259
                            $dimension = (int)$dimension * $pixelDensity;
1260
                        }
1261
                        $sourceRenderConfiguration['file.'][$dimensionKey] = $dimension;
1262
                        // Remove the stdWrap properties for dimension as they have been processed already above.
1263
                        unset($sourceRenderConfiguration['file.'][$dimensionKey . '.']);
1264
                    }
1265
                }
1266
                $sourceInfo = $this->getImgResource($sourceRenderConfiguration['file'], $sourceRenderConfiguration['file.']);
1267
                if ($sourceInfo) {
1268
                    $sourceConfiguration['width'] = $sourceInfo[0];
1269
                    $sourceConfiguration['height'] = $sourceInfo[1];
1270
                    $urlPrefix = '';
1271
                    if (parse_url($sourceInfo[3], PHP_URL_HOST) === null) {
1272
                        $urlPrefix = $tsfe->absRefPrefix;
1273
                    }
1274
                    $sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $sourceInfo[3]);
1275
                    $sourceConfiguration['selfClosingTagSlash'] = !empty($tsfe->xhtmlDoctype) ? ' /' : '';
1276
1277
                    $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1278
                    $oneSourceCollection = $markerTemplateEngine->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
1279
1280
                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImageSourceCollection'] ?? [] as $className) {
1281
                        $hookObject = GeneralUtility::makeInstance($className);
1282
                        if (!$hookObject instanceof ContentObjectOneSourceCollectionHookInterface) {
1283
                            throw new \UnexpectedValueException(
1284
                                '$hookObject must implement interface ' . ContentObjectOneSourceCollectionHookInterface::class,
1285
                                1380007853
1286
                            );
1287
                        }
1288
                        $oneSourceCollection = $hookObject->getOneSourceCollection((array)$sourceRenderConfiguration, (array)$sourceConfiguration, $oneSourceCollection, $this);
1289
                    }
1290
1291
                    $sourceCollection .= $oneSourceCollection;
1292
                }
1293
            }
1294
        }
1295
        return $sourceCollection;
1296
    }
1297
1298
    /**
1299
     * Wraps the input string in link-tags that opens the image in a new window.
1300
     *
1301
     * @param string $string String to wrap, probably an <img> tag
1302
     * @param string|File|FileReference $imageFile The original image file
1303
     * @param array $conf TypoScript properties for the "imageLinkWrap" function
1304
     * @return string The input string, $string, wrapped as configured.
1305
     * @see cImage()
1306
     * @internal This method should be used within TYPO3 Core only
1307
     */
1308
    public function imageLinkWrap($string, $imageFile, $conf)
1309
    {
1310
        $string = (string)$string;
1311
        $enable = isset($conf['enable.']) ? $this->stdWrap($conf['enable'], $conf['enable.']) : $conf['enable'];
1312
        if (!$enable) {
1313
            return $string;
1314
        }
1315
        $content = (string)$this->typoLink($string, $conf['typolink.']);
1316
        if (isset($conf['file.'])) {
1317
            $imageFile = $this->stdWrap($imageFile, $conf['file.']);
0 ignored issues
show
Bug introduced by
It seems like $imageFile can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $content of TYPO3\CMS\Frontend\Conte...jectRenderer::stdWrap() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1317
            $imageFile = $this->stdWrap(/** @scrutinizer ignore-type */ $imageFile, $conf['file.']);
Loading history...
1318
        }
1319
1320
        if ($imageFile instanceof File) {
1321
            $file = $imageFile;
1322
        } elseif ($imageFile instanceof FileReference) {
1323
            $file = $imageFile->getOriginalFile();
1324
        } else {
1325
            if (MathUtility::canBeInterpretedAsInteger($imageFile)) {
1326
                $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$imageFile);
1327
            } else {
1328
                $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObjectFromCombinedIdentifier($imageFile);
1329
            }
1330
        }
1331
1332
        // Create imageFileLink if not created with typolink
1333
        if ($content === $string && $file !== null) {
1334
            $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop'];
1335
            $parameters = [];
1336
            $sample = isset($conf['sample.']) ? $this->stdWrap($conf['sample'], $conf['sample.']) : $conf['sample'];
1337
            if ($sample) {
1338
                $parameters['sample'] = 1;
1339
            }
1340
            foreach ($parameterNames as $parameterName) {
1341
                if (isset($conf[$parameterName . '.'])) {
1342
                    $conf[$parameterName] = $this->stdWrap($conf[$parameterName], $conf[$parameterName . '.']);
1343
                }
1344
                if (isset($conf[$parameterName]) && $conf[$parameterName]) {
1345
                    $parameters[$parameterName] = $conf[$parameterName];
1346
                }
1347
            }
1348
            $parametersEncoded = base64_encode(serialize($parameters));
1349
            $hmac = GeneralUtility::hmac(implode('|', [$file->getUid(), $parametersEncoded]));
1350
            $params = '&md5=' . $hmac;
1351
            foreach (str_split($parametersEncoded, 64) as $index => $chunk) {
1352
                $params .= '&parameters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk);
1353
            }
1354
            $url = $this->getTypoScriptFrontendController()->absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params;
1355
            $directImageLink = isset($conf['directImageLink.']) ? $this->stdWrap($conf['directImageLink'], $conf['directImageLink.']) : $conf['directImageLink'];
1356
            if ($directImageLink) {
1357
                $imgResourceConf = [
1358
                    'file' => $imageFile,
1359
                    'file.' => $conf
1360
                ];
1361
                $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf);
1362
                if (!$url) {
1363
                    // If no imagemagick / gm is available
1364
                    $url = $imageFile;
1365
                }
1366
            }
1367
            // Create TARGET-attribute only if the right doctype is used
1368
            $target = '';
1369
            $xhtmlDocType = $this->getTypoScriptFrontendController()->xhtmlDoctype;
1370
            if ($xhtmlDocType !== 'xhtml_strict' && $xhtmlDocType !== 'xhtml_11') {
1371
                $target = isset($conf['target.'])
1372
                    ? (string)$this->stdWrap($conf['target'], $conf['target.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
1373
                    : (string)$conf['target'];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
1374
                if ($target === '') {
1375
                    $target = 'thePicture';
1376
                }
1377
            }
1378
            $a1 = '';
1379
            $a2 = '';
1380
            $conf['JSwindow'] = isset($conf['JSwindow.']) ? $this->stdWrap($conf['JSwindow'], $conf['JSwindow.']) : $conf['JSwindow'];
1381
            if ($conf['JSwindow']) {
1382
                if ($conf['JSwindow.']['altUrl'] || $conf['JSwindow.']['altUrl.']) {
1383
                    $altUrl = isset($conf['JSwindow.']['altUrl.']) ? $this->stdWrap($conf['JSwindow.']['altUrl'], $conf['JSwindow.']['altUrl.']) : $conf['JSwindow.']['altUrl'];
1384
                    if ($altUrl) {
1385
                        $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode($imageFile) . $params);
0 ignored issues
show
Bug introduced by
It seems like $imageFile can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $str of rawurlencode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1385
                        $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode(/** @scrutinizer ignore-type */ $imageFile) . $params);
Loading history...
1386
                    }
1387
                }
1388
1389
                $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf);
1390
                $JSwindowExpand = isset($conf['JSwindow.']['expand.']) ? $this->stdWrap($conf['JSwindow.']['expand'], $conf['JSwindow.']['expand.']) : $conf['JSwindow.']['expand'];
1391
                $offset = GeneralUtility::intExplode(',', $JSwindowExpand . ',');
1392
                $newWindow = isset($conf['JSwindow.']['newWindow.']) ? $this->stdWrap($conf['JSwindow.']['newWindow'], $conf['JSwindow.']['newWindow.']) : $conf['JSwindow.']['newWindow'];
1393
                $params = [
1394
                    'width' => ($processedFile->getProperty('width') + $offset[0]),
1395
                    'height' => ($processedFile->getProperty('height') + $offset[1]),
1396
                    'status' => '0',
1397
                    'menubar' => '0'
1398
                ];
1399
                // params override existing parameters from above, or add more
1400
                $windowParams = isset($conf['JSwindow.']['params.']) ? $this->stdWrap($conf['JSwindow.']['params'], $conf['JSwindow.']['params.']) : $conf['JSwindow.']['params'];
1401
                $windowParams = explode(',', $windowParams);
1402
                foreach ($windowParams as $windowParam) {
1403
                    [$paramKey, $paramValue] = explode('=', $windowParam);
1404
                    if ($paramValue !== '') {
1405
                        $params[$paramKey] = $paramValue;
1406
                    } else {
1407
                        unset($params[$paramKey]);
1408
                    }
1409
                }
1410
                $paramString = '';
1411
                foreach ($params as $paramKey => $paramValue) {
1412
                    $paramString .= htmlspecialchars($paramKey) . '=' . htmlspecialchars($paramValue) . ',';
1413
                }
1414
1415
                $onClick = 'openPic('
1416
                    . GeneralUtility::quoteJSvalue($this->getTypoScriptFrontendController()->baseUrlWrap($url)) . ','
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $url of TYPO3\CMS\Frontend\Contr...ntroller::baseUrlWrap() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1416
                    . GeneralUtility::quoteJSvalue($this->getTypoScriptFrontendController()->baseUrlWrap(/** @scrutinizer ignore-type */ $url)) . ','
Loading history...
1417
                    . '\'' . ($newWindow ? md5($url) : 'thePicture') . '\','
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $str of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1417
                    . '\'' . ($newWindow ? md5(/** @scrutinizer ignore-type */ $url) : 'thePicture') . '\','
Loading history...
1418
                    . GeneralUtility::quoteJSvalue(rtrim($paramString, ',')) . '); return false;';
1419
                $a1 = '<a href="' . htmlspecialchars($url) . '"'
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type TYPO3\CMS\Core\Resource\File and TYPO3\CMS\Core\Resource\FileReference; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1419
                $a1 = '<a href="' . htmlspecialchars(/** @scrutinizer ignore-type */ $url) . '"'
Loading history...
1420
                    . ' onclick="' . htmlspecialchars($onClick) . '"'
1421
                    . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
1422
                    . $this->getTypoScriptFrontendController()->ATagParams . '>';
1423
                $a2 = '</a>';
1424
                $this->getTypoScriptFrontendController()->setJS('openPic');
1425
            } else {
1426
                $conf['linkParams.']['directImageLink'] = (bool)$conf['directImageLink'];
1427
                $conf['linkParams.']['parameter'] = $url;
1428
                $string = $this->typoLink($string, $conf['linkParams.']);
1429
            }
1430
            if (isset($conf['stdWrap.'])) {
1431
                $string = $this->stdWrap($string, $conf['stdWrap.']);
1432
            }
1433
            $content = $a1 . $string . $a2;
1434
        }
1435
        return $content;
1436
    }
1437
1438
    /**
1439
     * Sets the SYS_LASTCHANGED timestamp if input timestamp is larger than current value.
1440
     * The SYS_LASTCHANGED timestamp can be used by various caching/indexing applications to determine if the page has new content.
1441
     * Therefore you should call this function with the last-changed timestamp of any element you display.
1442
     *
1443
     * @param int $tstamp Unix timestamp (number of seconds since 1970)
1444
     * @see TypoScriptFrontendController::setSysLastChanged()
1445
     */
1446
    public function lastChanged($tstamp)
1447
    {
1448
        $tstamp = (int)$tstamp;
1449
        $tsfe = $this->getTypoScriptFrontendController();
1450
        if ($tstamp > (int)$tsfe->register['SYS_LASTCHANGED']) {
1451
            $tsfe->register['SYS_LASTCHANGED'] = $tstamp;
1452
        }
1453
    }
1454
1455
    /**
1456
     * Wraps the input string by the $wrap value and implements the "linkWrap" data type as well.
1457
     * 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.
1458
     *
1459
     * @param string $content Input string
1460
     * @param string $wrap A string where the first two parts separated by "|" (vertical line) will be wrapped around the input string
1461
     * @return string Wrapped output string
1462
     * @see wrap()
1463
     * @see cImage()
1464
     * @deprecated will be removed in TYPO3 v11.0.
1465
     */
1466
    public function linkWrap($content, $wrap)
1467
    {
1468
        trigger_error('cObj->linkWrap() will be removed in TYPO3 v11.0. This functionality is integrated into ImageContentObject.', E_USER_DEPRECATED);
1469
        $wrapArr = explode('|', $wrap);
1470
        if (preg_match('/\\{([0-9]*)\\}/', $wrapArr[0], $reg)) {
1471
            $uid = $this->getTypoScriptFrontendController()->tmpl->rootLine[$reg[1]]['uid'] ?? null;
1472
            if ($uid) {
1473
                $wrapArr[0] = str_replace($reg[0], $uid, $wrapArr[0]);
1474
            }
1475
        }
1476
        return trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
1477
    }
1478
1479
    /**
1480
     * An abstraction method which creates an alt or title parameter for an HTML img, applet, area or input element and the FILE content element.
1481
     * From the $conf array it implements the properties "altText", "titleText" and "longdescURL"
1482
     *
1483
     * @param array $conf TypoScript configuration properties
1484
     * @param bool $longDesc If set, the longdesc attribute will be generated - must only be used for img elements!
1485
     * @return string Parameter string containing alt and title parameters (if any)
1486
     * @deprecated will be removed in TYPO3 v11.0.
1487
     */
1488
    public function getAltParam($conf, $longDesc = true)
1489
    {
1490
        trigger_error('cObj->getAltParam() will be removed in TYPO3 v11.0. This functionality is integrated into ImageContentObject.', E_USER_DEPRECATED);
1491
        $altText = isset($conf['altText.']) ? trim($this->stdWrap($conf['altText'], $conf['altText.'])) : trim($conf['altText']);
1492
        $titleText = isset($conf['titleText.']) ? trim($this->stdWrap($conf['titleText'], $conf['titleText.'])) : trim($conf['titleText']);
1493
        if (isset($conf['longdescURL.']) && $this->getTypoScriptFrontendController()->config['config']['doctype'] !== 'html5') {
1494
            $longDescUrl = $this->typoLink_URL($conf['longdescURL.']);
1495
        } else {
1496
            $longDescUrl = trim($conf['longdescURL']);
1497
        }
1498
        $longDescUrl = strip_tags($longDescUrl);
1499
1500
        // "alt":
1501
        $altParam = ' alt="' . htmlspecialchars($altText) . '"';
1502
        // "title":
1503
        $emptyTitleHandling = isset($conf['emptyTitleHandling.']) ? $this->stdWrap($conf['emptyTitleHandling'], $conf['emptyTitleHandling.']) : $conf['emptyTitleHandling'];
1504
        // Choices: 'keepEmpty' | 'useAlt' | 'removeAttr'
1505
        if ($titleText || $emptyTitleHandling === 'keepEmpty') {
1506
            $altParam .= ' title="' . htmlspecialchars($titleText) . '"';
1507
        } elseif (!$titleText && $emptyTitleHandling === 'useAlt') {
1508
            $altParam .= ' title="' . htmlspecialchars($altText) . '"';
1509
        }
1510
        // "longDesc" URL
1511
        if ($longDesc && !empty($longDescUrl)) {
1512
            $altParam .= ' longdesc="' . htmlspecialchars($longDescUrl) . '"';
1513
        }
1514
        return $altParam;
1515
    }
1516
1517
    /**
1518
     * An abstraction method to add parameters to an A tag.
1519
     * Uses the ATagParams property.
1520
     *
1521
     * @param array $conf TypoScript configuration properties
1522
     * @param bool|int $addGlobal If set, will add the global config.ATagParams to the link
1523
     * @return string String containing the parameters to the A tag (if non empty, with a leading space)
1524
     * @see typolink()
1525
     */
1526
    public function getATagParams($conf, $addGlobal = 1)
1527
    {
1528
        $aTagParams = '';
1529
        if ($conf['ATagParams.'] ?? false) {
1530
            $aTagParams = ' ' . $this->stdWrap($conf['ATagParams'], $conf['ATagParams.']);
1531
        } elseif ($conf['ATagParams'] ?? false) {
1532
            $aTagParams = ' ' . $conf['ATagParams'];
1533
        }
1534
        if ($addGlobal) {
1535
            $aTagParams = ' ' . trim($this->getTypoScriptFrontendController()->ATagParams . $aTagParams);
1536
        }
1537
        // Extend params
1538
        $_params = [
1539
            'conf' => &$conf,
1540
            'aTagParams' => &$aTagParams
1541
        ];
1542
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ?? [] as $className) {
1543
            $processor = & GeneralUtility::makeInstance($className);
1544
            $aTagParams = $processor->process($_params, $this);
1545
        }
1546
1547
        $aTagParams = trim($aTagParams);
1548
        if (!empty($aTagParams)) {
1549
            $aTagParams = ' ' . $aTagParams;
1550
        }
1551
1552
        return $aTagParams;
1553
    }
1554
1555
    /**
1556
     * All extension links should ask this function for additional properties to their tags.
1557
     * Designed to add for instance an "onclick" property for site tracking systems.
1558
     *
1559
     * @param string $URL URL of the website
1560
     * @param string $TYPE
1561
     * @return string The additional tag properties
1562
     * @internal This method will be removed as it serves no purpose anymore in TYPO3 v11.0
1563
     */
1564
    public function extLinkATagParams($URL, $TYPE)
1565
    {
1566
        $out = '';
1567
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['extLinkATagParamsHandler'])) {
1568
            trigger_error('The hook $TYPO3_CONF_VARS[SC_OPTIONS][tslib/class.tslib_content.php][extLinkATagParamsHandler] will be removed in TYPO3 v11.0. Use a custom LinkHandler instead.', E_USER_DEPRECATED);
1569
            $extLinkATagParamsHandler = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['extLinkATagParamsHandler']);
1570
            if (method_exists($extLinkATagParamsHandler, 'main')) {
1571
                $out .= trim($extLinkATagParamsHandler->main($URL, $TYPE, $this));
1572
            }
1573
        }
1574
        return trim($out) ? ' ' . trim($out) : '';
1575
    }
1576
1577
    /***********************************************
1578
     *
1579
     * HTML template processing functions
1580
     *
1581
     ***********************************************/
1582
1583
    /**
1584
     * Sets the current file object during iterations over files.
1585
     *
1586
     * @param File $fileObject The file object.
1587
     */
1588
    public function setCurrentFile($fileObject)
1589
    {
1590
        $this->currentFile = $fileObject;
1591
    }
1592
1593
    /**
1594
     * Gets the current file object during iterations over files.
1595
     *
1596
     * @return File The current file object.
1597
     */
1598
    public function getCurrentFile()
1599
    {
1600
        return $this->currentFile;
1601
    }
1602
1603
    /***********************************************
1604
     *
1605
     * "stdWrap" + sub functions
1606
     *
1607
     ***********************************************/
1608
    /**
1609
     * The "stdWrap" function. This is the implementation of what is known as "stdWrap properties" in TypoScript.
1610
     * Basically "stdWrap" performs some processing of a value based on properties in the input $conf array(holding the TypoScript "stdWrap properties")
1611
     * 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.
1612
     *
1613
     * If $this->alternativeData is an array it's used instead of the $this->data array in ->getData
1614
     *
1615
     * @param string $content Input value undergoing processing in this function. Possibly substituted by other values fetched from another source.
1616
     * @param array $conf TypoScript "stdWrap properties".
1617
     * @return string The processed input value
1618
     */
1619
    public function stdWrap($content = '', $conf = [])
1620
    {
1621
        $content = (string)$content;
1622
        // If there is any hook object, activate all of the process and override functions.
1623
        // The hook interface ContentObjectStdWrapHookInterface takes care that all 4 methods exist.
1624
        if ($this->stdWrapHookObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->stdWrapHookObjects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1625
            $conf['stdWrapPreProcess'] = 1;
1626
            $conf['stdWrapOverride'] = 1;
1627
            $conf['stdWrapProcess'] = 1;
1628
            $conf['stdWrapPostProcess'] = 1;
1629
        }
1630
1631
        if (!is_array($conf) || !$conf) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
1632
            return $content;
1633
        }
1634
1635
        // Cache handling
1636
        if (isset($conf['cache.']) && is_array($conf['cache.'])) {
1637
            $conf['cache.']['key'] = $this->stdWrap($conf['cache.']['key'], $conf['cache.']['key.']);
1638
            $conf['cache.']['tags'] = $this->stdWrap($conf['cache.']['tags'], $conf['cache.']['tags.']);
1639
            $conf['cache.']['lifetime'] = $this->stdWrap($conf['cache.']['lifetime'], $conf['cache.']['lifetime.']);
1640
            $conf['cacheRead'] = 1;
1641
            $conf['cacheStore'] = 1;
1642
        }
1643
        // The configuration is sorted and filtered by intersection with the defined stdWrapOrder.
1644
        $sortedConf = array_keys(array_intersect_key($this->stdWrapOrder, $conf));
1645
        // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions
1646
        $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap';
1647
        // Additional Array to check whether a function has already been executed
1648
        $isExecuted = [];
1649
        // Additional switch to make sure 'required', 'if' and 'fieldRequired'
1650
        // will still stop rendering immediately in case they return FALSE
1651
        $this->stdWrapRecursionLevel++;
1652
        $this->stopRendering[$this->stdWrapRecursionLevel] = false;
1653
        // execute each function in the predefined order
1654
        foreach ($sortedConf as $stdWrapName) {
1655
            // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped
1656
            if ((!isset($isExecuted[$stdWrapName]) || !$isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapRecursionLevel]) {
1657
                $functionName = rtrim($stdWrapName, '.');
1658
                $functionProperties = $functionName . '.';
1659
                $functionType = $this->stdWrapOrder[$functionName] ?? null;
1660
                // If there is any code on the next level, check if it contains "official" stdWrap functions
1661
                // if yes, execute them first - will make each function stdWrap aware
1662
                // so additional stdWrap calls within the functions can be removed, since the result will be the same
1663
                if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) {
1664
                    if (array_intersect_key($this->stdWrapOrder, $conf[$functionProperties])) {
1665
                        // Check if there's already content available before processing
1666
                        // any ifEmpty or ifBlank stdWrap properties
1667
                        if (($functionName === 'ifBlank' && $content !== '') ||
1668
                            ($functionName === 'ifEmpty' && trim($content) !== '')) {
1669
                            continue;
1670
                        }
1671
1672
                        $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties] ?? []);
1673
                    }
1674
                }
1675
                // Check if key is still containing something, since it might have been changed by next level stdWrap before
1676
                if ((isset($conf[$functionName]) || $conf[$functionProperties])
1677
                    && ($functionType !== 'boolean' || $conf[$functionName])
1678
                ) {
1679
                    // Get just that part of $conf that is needed for the particular function
1680
                    $singleConf = [
1681
                        $functionName => $conf[$functionName] ?? null,
1682
                        $functionProperties => $conf[$functionProperties] ?? null
1683
                    ];
1684
                    // Hand over the whole $conf array to the stdWrapHookObjects
1685
                    if ($functionType === 'hook') {
1686
                        $singleConf = $conf;
1687
                    }
1688
                    // Add both keys - with and without the dot - to the set of executed functions
1689
                    $isExecuted[$functionName] = true;
1690
                    $isExecuted[$functionProperties] = true;
1691
                    // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array
1692
                    $functionName = 'stdWrap_' . $functionName;
1693
                    $content = $this->{$functionName}($content, $singleConf);
1694
                } elseif ($functionType === 'boolean' && !$conf[$functionName]) {
1695
                    $isExecuted[$functionName] = true;
1696
                    $isExecuted[$functionProperties] = true;
1697
                }
1698
            }
1699
        }
1700
        unset($this->stopRendering[$this->stdWrapRecursionLevel]);
1701
        $this->stdWrapRecursionLevel--;
1702
1703
        return $content;
1704
    }
1705
1706
    /**
1707
     * Gets a configuration value by passing them through stdWrap first and taking a default value if stdWrap doesn't yield a result.
1708
     *
1709
     * @param string $key The config variable key (from TS array).
1710
     * @param array $config The TypoScript array.
1711
     * @param string $defaultValue Optional default value.
1712
     * @return string Value of the config variable
1713
     */
1714
    public function stdWrapValue($key, array $config, $defaultValue = '')
1715
    {
1716
        if (isset($config[$key])) {
1717
            if (!isset($config[$key . '.'])) {
1718
                return $config[$key];
1719
            }
1720
        } elseif (isset($config[$key . '.'])) {
1721
            $config[$key] = '';
1722
        } else {
1723
            return $defaultValue;
1724
        }
1725
        $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']);
1726
        return $stdWrapped ?: $defaultValue;
1727
    }
1728
1729
    /**
1730
     * stdWrap pre process hook
1731
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1732
     * this hook will execute functions before any other stdWrap function can modify anything
1733
     *
1734
     * @param string $content Input value undergoing processing in these functions.
1735
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1736
     * @return string The processed input value
1737
     */
1738
    public function stdWrap_stdWrapPreProcess($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_stdWrapPreProcess" is not in camel caps format
Loading history...
1739
    {
1740
        foreach ($this->stdWrapHookObjects as $hookObject) {
1741
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1742
            $content = $hookObject->stdWrapPreProcess($content, $conf, $this);
1743
        }
1744
        return $content;
1745
    }
1746
1747
    /**
1748
     * Check if content was cached before (depending on the given cache key)
1749
     *
1750
     * @param string $content Input value undergoing processing in these functions.
1751
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1752
     * @return string The processed input value
1753
     */
1754
    public function stdWrap_cacheRead($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_cacheRead" is not in camel caps format
Loading history...
1755
    {
1756
        if (!isset($conf['cache.'])) {
1757
            return $content;
1758
        }
1759
        $result = $this->getFromCache($conf['cache.']);
1760
        return $result === false ? $content : $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result === false ? $content : $result also could return the type true which is incompatible with the documented return type string.
Loading history...
1761
    }
1762
1763
    /**
1764
     * Add tags to page cache (comma-separated list)
1765
     *
1766
     * @param string $content Input value undergoing processing in these functions.
1767
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1768
     * @return string The processed input value
1769
     */
1770
    public function stdWrap_addPageCacheTags($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_addPageCacheTags" is not in camel caps format
Loading history...
1771
    {
1772
        $tags = isset($conf['addPageCacheTags.'])
1773
            ? $this->stdWrap($conf['addPageCacheTags'], $conf['addPageCacheTags.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
1774
            : $conf['addPageCacheTags'];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
1775
        if (!empty($tags)) {
1776
            $cacheTags = GeneralUtility::trimExplode(',', $tags, true);
1777
            $this->getTypoScriptFrontendController()->addCacheTags($cacheTags);
1778
        }
1779
        return $content;
1780
    }
1781
1782
    /**
1783
     * setContentToCurrent
1784
     * actually it just does the contrary: Sets the value of 'current' based on current content
1785
     *
1786
     * @param string $content Input value undergoing processing in this function.
1787
     * @return string The processed input value
1788
     */
1789
    public function stdWrap_setContentToCurrent($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_setContentToCurrent" is not in camel caps format
Loading history...
1790
    {
1791
        $this->data[$this->currentValKey] = $content;
1792
        return $content;
1793
    }
1794
1795
    /**
1796
     * setCurrent
1797
     * Sets the value of 'current' based on the outcome of stdWrap operations
1798
     *
1799
     * @param string $content Input value undergoing processing in this function.
1800
     * @param array $conf stdWrap properties for setCurrent.
1801
     * @return string The processed input value
1802
     */
1803
    public function stdWrap_setCurrent($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_setCurrent" is not in camel caps format
Loading history...
1804
    {
1805
        $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null;
1806
        return $content;
1807
    }
1808
1809
    /**
1810
     * lang
1811
     * Translates content based on the language currently used by the FE
1812
     *
1813
     * @param string $content Input value undergoing processing in this function.
1814
     * @param array $conf stdWrap properties for lang.
1815
     * @return string The processed input value
1816
     */
1817
    public function stdWrap_lang($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_lang" is not in camel caps format
Loading history...
1818
    {
1819
        $currentLanguageCode = $this->getTypoScriptFrontendController()->getLanguage()->getTypo3Language();
1820
        if ($currentLanguageCode && isset($conf['lang.'][$currentLanguageCode])) {
1821
            $content = $conf['lang.'][$currentLanguageCode];
1822
        }
1823
        return $content;
1824
    }
1825
1826
    /**
1827
     * data
1828
     * Gets content from different sources based on getText functions, makes use of alternativeData, when set
1829
     *
1830
     * @param string $content Input value undergoing processing in this function.
1831
     * @param array $conf stdWrap properties for data.
1832
     * @return string The processed input value
1833
     */
1834
    public function stdWrap_data($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_data" is not in camel caps format
Loading history...
1835
    {
1836
        $content = $this->getData($conf['data'], is_array($this->alternativeData) ? $this->alternativeData : $this->data);
0 ignored issues
show
introduced by
The condition is_array($this->alternativeData) is always false.
Loading history...
1837
        // This must be unset directly after
1838
        $this->alternativeData = '';
1839
        return $content;
1840
    }
1841
1842
    /**
1843
     * field
1844
     * Gets content from a DB field
1845
     *
1846
     * @param string $content Input value undergoing processing in this function.
1847
     * @param array $conf stdWrap properties for field.
1848
     * @return string The processed input value
1849
     */
1850
    public function stdWrap_field($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_field" is not in camel caps format
Loading history...
1851
    {
1852
        return $this->getFieldVal($conf['field']);
1853
    }
1854
1855
    /**
1856
     * current
1857
     * Gets content that has been previously set as 'current'
1858
     * Can be set via setContentToCurrent or setCurrent or will be set automatically i.e. inside the split function
1859
     *
1860
     * @param string $content Input value undergoing processing in this function.
1861
     * @param array $conf stdWrap properties for current.
1862
     * @return string The processed input value
1863
     */
1864
    public function stdWrap_current($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_current" is not in camel caps format
Loading history...
1865
    {
1866
        return $this->data[$this->currentValKey];
1867
    }
1868
1869
    /**
1870
     * cObject
1871
     * Will replace the content with the value of an official TypoScript cObject
1872
     * like TEXT, COA, HMENU
1873
     *
1874
     * @param string $content Input value undergoing processing in this function.
1875
     * @param array $conf stdWrap properties for cObject.
1876
     * @return string The processed input value
1877
     */
1878
    public function stdWrap_cObject($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_cObject" is not in camel caps format
Loading history...
1879
    {
1880
        return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject');
1881
    }
1882
1883
    /**
1884
     * numRows
1885
     * Counts the number of returned records of a DB operation
1886
     * makes use of select internally
1887
     *
1888
     * @param string $content Input value undergoing processing in this function.
1889
     * @param array $conf stdWrap properties for numRows.
1890
     * @return string The processed input value
1891
     */
1892
    public function stdWrap_numRows($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_numRows" is not in camel caps format
Loading history...
1893
    {
1894
        return $this->numRows($conf['numRows.']);
1895
    }
1896
1897
    /**
1898
     * preUserFunc
1899
     * Will execute a user public function before the content will be modified by any other stdWrap function
1900
     *
1901
     * @param string $content Input value undergoing processing in this function.
1902
     * @param array $conf stdWrap properties for preUserFunc.
1903
     * @return string The processed input value
1904
     */
1905
    public function stdWrap_preUserFunc($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_preUserFunc" is not in camel caps format
Loading history...
1906
    {
1907
        return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'], $content);
1908
    }
1909
1910
    /**
1911
     * stdWrap override hook
1912
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
1913
     * this hook will execute functions on existing content but still before the content gets modified or replaced
1914
     *
1915
     * @param string $content Input value undergoing processing in these functions.
1916
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
1917
     * @return string The processed input value
1918
     */
1919
    public function stdWrap_stdWrapOverride($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_stdWrapOverride" is not in camel caps format
Loading history...
1920
    {
1921
        foreach ($this->stdWrapHookObjects as $hookObject) {
1922
            /** @var ContentObjectStdWrapHookInterface $hookObject */
1923
            $content = $hookObject->stdWrapOverride($content, $conf, $this);
1924
        }
1925
        return $content;
1926
    }
1927
1928
    /**
1929
     * override
1930
     * Will override the current value of content with its own value'
1931
     *
1932
     * @param string $content Input value undergoing processing in this function.
1933
     * @param array $conf stdWrap properties for override.
1934
     * @return string The processed input value
1935
     */
1936
    public function stdWrap_override($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_override" is not in camel caps format
Loading history...
1937
    {
1938
        if (trim($conf['override'] ?? false)) {
0 ignored issues
show
Bug introduced by
It seems like $conf['override'] ?? false can also be of type false; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1938
        if (trim(/** @scrutinizer ignore-type */ $conf['override'] ?? false)) {
Loading history...
1939
            $content = $conf['override'];
1940
        }
1941
        return $content;
1942
    }
1943
1944
    /**
1945
     * preIfEmptyListNum
1946
     * Gets a value off a CSV list before the following ifEmpty check
1947
     * 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
1948
     *
1949
     * @param string $content Input value undergoing processing in this function.
1950
     * @param array $conf stdWrap properties for preIfEmptyListNum.
1951
     * @return string The processed input value
1952
     */
1953
    public function stdWrap_preIfEmptyListNum($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_preIfEmptyListNum" is not in camel caps format
Loading history...
1954
    {
1955
        return $this->listNum($content, $conf['preIfEmptyListNum'] ?? null, $conf['preIfEmptyListNum.']['splitChar'] ?? null);
1956
    }
1957
1958
    /**
1959
     * ifNull
1960
     * Will set content to a replacement value in case the value of content is NULL
1961
     *
1962
     * @param string|null $content Input value undergoing processing in this function.
1963
     * @param array $conf stdWrap properties for ifNull.
1964
     * @return string The processed input value
1965
     */
1966
    public function stdWrap_ifNull($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_ifNull" is not in camel caps format
Loading history...
1967
    {
1968
        return $content ?? $conf['ifNull'];
1969
    }
1970
1971
    /**
1972
     * ifEmpty
1973
     * Will set content to a replacement value in case the trimmed value of content returns FALSE
1974
     * 0 (zero) will be replaced as well
1975
     *
1976
     * @param string $content Input value undergoing processing in this function.
1977
     * @param array $conf stdWrap properties for ifEmpty.
1978
     * @return string The processed input value
1979
     */
1980
    public function stdWrap_ifEmpty($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_ifEmpty" is not in camel caps format
Loading history...
1981
    {
1982
        if (!trim($content)) {
1983
            $content = $conf['ifEmpty'];
1984
        }
1985
        return $content;
1986
    }
1987
1988
    /**
1989
     * ifBlank
1990
     * Will set content to a replacement value in case the trimmed value of content has no length
1991
     * 0 (zero) will not be replaced
1992
     *
1993
     * @param string $content Input value undergoing processing in this function.
1994
     * @param array $conf stdWrap properties for ifBlank.
1995
     * @return string The processed input value
1996
     */
1997
    public function stdWrap_ifBlank($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_ifBlank" is not in camel caps format
Loading history...
1998
    {
1999
        if (trim($content) === '') {
2000
            $content = $conf['ifBlank'];
2001
        }
2002
        return $content;
2003
    }
2004
2005
    /**
2006
     * listNum
2007
     * Gets a value off a CSV list after ifEmpty check
2008
     * Might return an empty value in case the CSV does not contain a value at the position given by listNum
2009
     * Use preIfEmptyListNum to avoid that behaviour
2010
     *
2011
     * @param string $content Input value undergoing processing in this function.
2012
     * @param array $conf stdWrap properties for listNum.
2013
     * @return string The processed input value
2014
     */
2015
    public function stdWrap_listNum($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_listNum" is not in camel caps format
Loading history...
2016
    {
2017
        return $this->listNum($content, $conf['listNum'] ?? null, $conf['listNum.']['splitChar'] ?? null);
2018
    }
2019
2020
    /**
2021
     * trim
2022
     * Cuts off any whitespace at the beginning and the end of the content
2023
     *
2024
     * @param string $content Input value undergoing processing in this function.
2025
     * @return string The processed input value
2026
     */
2027
    public function stdWrap_trim($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_trim" is not in camel caps format
Loading history...
2028
    {
2029
        return trim($content);
2030
    }
2031
2032
    /**
2033
     * strPad
2034
     * Will return a string padded left/right/on both sides, based on configuration given as stdWrap properties
2035
     *
2036
     * @param string $content Input value undergoing processing in this function.
2037
     * @param array $conf stdWrap properties for strPad.
2038
     * @return string The processed input value
2039
     */
2040
    public function stdWrap_strPad($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_strPad" is not in camel caps format
Loading history...
2041
    {
2042
        // Must specify a length in conf for this to make sense
2043
        $length = 0;
2044
        // Padding with space is PHP-default
2045
        $padWith = ' ';
2046
        // Padding on the right side is PHP-default
2047
        $padType = STR_PAD_RIGHT;
2048
        if (!empty($conf['strPad.']['length'])) {
2049
            $length = isset($conf['strPad.']['length.']) ? $this->stdWrap($conf['strPad.']['length'], $conf['strPad.']['length.']) : $conf['strPad.']['length'];
2050
            $length = (int)$length;
2051
        }
2052
        if (isset($conf['strPad.']['padWith']) && (string)$conf['strPad.']['padWith'] !== '') {
2053
            $padWith = isset($conf['strPad.']['padWith.']) ? $this->stdWrap($conf['strPad.']['padWith'], $conf['strPad.']['padWith.']) : $conf['strPad.']['padWith'];
2054
        }
2055
        if (!empty($conf['strPad.']['type'])) {
2056
            $type = isset($conf['strPad.']['type.']) ? $this->stdWrap($conf['strPad.']['type'], $conf['strPad.']['type.']) : $conf['strPad.']['type'];
2057
            if (strtolower($type) === 'left') {
2058
                $padType = STR_PAD_LEFT;
2059
            } elseif (strtolower($type) === 'both') {
2060
                $padType = STR_PAD_BOTH;
2061
            }
2062
        }
2063
        return str_pad($content, $length, $padWith, $padType);
2064
    }
2065
2066
    /**
2067
     * stdWrap
2068
     * A recursive call of the stdWrap function set
2069
     * This enables the user to execute stdWrap functions in another than the predefined order
2070
     * It modifies the content, not the property
2071
     * while the new feature of chained stdWrap functions modifies the property and not the content
2072
     *
2073
     * @param string $content Input value undergoing processing in this function.
2074
     * @param array $conf stdWrap properties for stdWrap.
2075
     * @return string The processed input value
2076
     */
2077
    public function stdWrap_stdWrap($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_stdWrap" is not in camel caps format
Loading history...
2078
    {
2079
        return $this->stdWrap($content, $conf['stdWrap.']);
2080
    }
2081
2082
    /**
2083
     * stdWrap process hook
2084
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2085
     * this hook executes functions directly after the recursive stdWrap function call but still before the content gets modified
2086
     *
2087
     * @param string $content Input value undergoing processing in these functions.
2088
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2089
     * @return string The processed input value
2090
     */
2091
    public function stdWrap_stdWrapProcess($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_stdWrapProcess" is not in camel caps format
Loading history...
2092
    {
2093
        foreach ($this->stdWrapHookObjects as $hookObject) {
2094
            /** @var ContentObjectStdWrapHookInterface $hookObject */
2095
            $content = $hookObject->stdWrapProcess($content, $conf, $this);
2096
        }
2097
        return $content;
2098
    }
2099
2100
    /**
2101
     * required
2102
     * Will immediately stop rendering and return an empty value
2103
     * when there is no content at this point
2104
     *
2105
     * @param string $content Input value undergoing processing in this function.
2106
     * @return string The processed input value
2107
     */
2108
    public function stdWrap_required($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_required" is not in camel caps format
Loading history...
2109
    {
2110
        if ((string)$content === '') {
2111
            $content = '';
2112
            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
2113
        }
2114
        return $content;
2115
    }
2116
2117
    /**
2118
     * if
2119
     * Will immediately stop rendering and return an empty value
2120
     * when the result of the checks returns FALSE
2121
     *
2122
     * @param string $content Input value undergoing processing in this function.
2123
     * @param array $conf stdWrap properties for if.
2124
     * @return string The processed input value
2125
     */
2126
    public function stdWrap_if($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_if" is not in camel caps format
Loading history...
2127
    {
2128
        if (empty($conf['if.']) || $this->checkIf($conf['if.'])) {
2129
            return $content;
2130
        }
2131
        $this->stopRendering[$this->stdWrapRecursionLevel] = true;
2132
        return '';
2133
    }
2134
2135
    /**
2136
     * fieldRequired
2137
     * Will immediately stop rendering and return an empty value
2138
     * when there is no content in the field given by fieldRequired
2139
     *
2140
     * @param string $content Input value undergoing processing in this function.
2141
     * @param array $conf stdWrap properties for fieldRequired.
2142
     * @return string The processed input value
2143
     */
2144
    public function stdWrap_fieldRequired($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_fieldRequired" is not in camel caps format
Loading history...
2145
    {
2146
        if (!trim($this->data[$conf['fieldRequired'] ?? null] ?? '')) {
2147
            $content = '';
2148
            $this->stopRendering[$this->stdWrapRecursionLevel] = true;
2149
        }
2150
        return $content;
2151
    }
2152
2153
    /**
2154
     * stdWrap csConv: Converts the input to UTF-8
2155
     *
2156
     * The character set of the input must be specified. Returns the input if
2157
     * matters go wrong, for example if an invalid character set is given.
2158
     *
2159
     * @param string $content The string to convert.
2160
     * @param array $conf stdWrap properties for csConv.
2161
     * @return string The processed input.
2162
     */
2163
    public function stdWrap_csConv($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_csConv" is not in camel caps format
Loading history...
2164
    {
2165
        if (!empty($conf['csConv'])) {
2166
            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv'])));
2167
            return $output !== false && $output !== '' ? $output : $content;
2168
        }
2169
        return $content;
2170
    }
2171
2172
    /**
2173
     * parseFunc
2174
     * Will parse the content based on functions given as stdWrap properties
2175
     * Heavily used together with RTE based content
2176
     *
2177
     * @param string $content Input value undergoing processing in this function.
2178
     * @param array $conf stdWrap properties for parseFunc.
2179
     * @return string The processed input value
2180
     */
2181
    public function stdWrap_parseFunc($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_parseFunc" is not in camel caps format
Loading history...
2182
    {
2183
        return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']);
2184
    }
2185
2186
    /**
2187
     * HTMLparser
2188
     * Will parse HTML content based on functions given as stdWrap properties
2189
     * Heavily used together with RTE based content
2190
     *
2191
     * @param string $content Input value undergoing processing in this function.
2192
     * @param array $conf stdWrap properties for HTMLparser.
2193
     * @return string The processed input value
2194
     */
2195
    public function stdWrap_HTMLparser($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_HTMLparser" is not in camel caps format
Loading history...
2196
    {
2197
        if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) {
2198
            $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']);
2199
        }
2200
        return $content;
2201
    }
2202
2203
    /**
2204
     * split
2205
     * Will split the content by a given token and treat the results separately
2206
     * Automatically fills 'current' with a single result
2207
     *
2208
     * @param string $content Input value undergoing processing in this function.
2209
     * @param array $conf stdWrap properties for split.
2210
     * @return string The processed input value
2211
     */
2212
    public function stdWrap_split($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_split" is not in camel caps format
Loading history...
2213
    {
2214
        return $this->splitObj($content, $conf['split.']);
2215
    }
2216
2217
    /**
2218
     * replacement
2219
     * Will execute replacements on the content (optionally with preg-regex)
2220
     *
2221
     * @param string $content Input value undergoing processing in this function.
2222
     * @param array $conf stdWrap properties for replacement.
2223
     * @return string The processed input value
2224
     */
2225
    public function stdWrap_replacement($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_replacement" is not in camel caps format
Loading history...
2226
    {
2227
        return $this->replacement($content, $conf['replacement.']);
2228
    }
2229
2230
    /**
2231
     * prioriCalc
2232
     * Will use the content as a mathematical term and calculate the result
2233
     * Can be set to 1 to just get a calculated value or 'intval' to get the integer of the result
2234
     *
2235
     * @param string $content Input value undergoing processing in this function.
2236
     * @param array $conf stdWrap properties for prioriCalc.
2237
     * @return string The processed input value
2238
     */
2239
    public function stdWrap_prioriCalc($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_prioriCalc" is not in camel caps format
Loading history...
2240
    {
2241
        $content = MathUtility::calculateWithParentheses($content);
2242
        if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') {
2243
            $content = (int)$content;
2244
        }
2245
        return $content;
2246
    }
2247
2248
    /**
2249
     * char
2250
     * Returns a one-character string containing the character specified by ascii code.
2251
     *
2252
     * Reliable results only for character codes in the integer range 0 - 127.
2253
     *
2254
     * @see http://php.net/manual/en/function.chr.php
2255
     * @param string $content Input value undergoing processing in this function.
2256
     * @param array $conf stdWrap properties for char.
2257
     * @return string The processed input value
2258
     */
2259
    public function stdWrap_char($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_char" is not in camel caps format
Loading history...
2260
    {
2261
        return chr((int)$conf['char']);
2262
    }
2263
2264
    /**
2265
     * intval
2266
     * Will return an integer value of the current content
2267
     *
2268
     * @param string $content Input value undergoing processing in this function.
2269
     * @return string The processed input value
2270
     */
2271
    public function stdWrap_intval($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_intval" is not in camel caps format
Loading history...
2272
    {
2273
        return (int)$content;
2274
    }
2275
2276
    /**
2277
     * Will return a hashed value of the current content
2278
     *
2279
     * @param string $content Input value undergoing processing in this function.
2280
     * @param array $conf stdWrap properties for hash.
2281
     * @return string The processed input value
2282
     * @link http://php.net/manual/de/function.hash-algos.php for a list of supported hash algorithms
2283
     */
2284
    public function stdWrap_hash($content = '', array $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_hash" is not in camel caps format
Loading history...
2285
    {
2286
        $algorithm = isset($conf['hash.']) ? $this->stdWrap($conf['hash'], $conf['hash.']) : $conf['hash'];
2287
        if (function_exists('hash') && in_array($algorithm, hash_algos())) {
2288
            return hash($algorithm, $content);
2289
        }
2290
        // Non-existing hashing algorithm
2291
        return '';
2292
    }
2293
2294
    /**
2295
     * stdWrap_round will return a rounded number with ceil(), floor() or round(), defaults to round()
2296
     * Only the english number format is supported . (dot) as decimal point
2297
     *
2298
     * @param string $content Input value undergoing processing in this function.
2299
     * @param array $conf stdWrap properties for round.
2300
     * @return string The processed input value
2301
     */
2302
    public function stdWrap_round($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_round" is not in camel caps format
Loading history...
2303
    {
2304
        return $this->round($content, $conf['round.']);
2305
    }
2306
2307
    /**
2308
     * numberFormat
2309
     * Will return a formatted number based on configuration given as stdWrap properties
2310
     *
2311
     * @param string $content Input value undergoing processing in this function.
2312
     * @param array $conf stdWrap properties for numberFormat.
2313
     * @return string The processed input value
2314
     */
2315
    public function stdWrap_numberFormat($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_numberFormat" is not in camel caps format
Loading history...
2316
    {
2317
        return $this->numberFormat($content, $conf['numberFormat.'] ?? []);
0 ignored issues
show
Bug introduced by
$content of type string is incompatible with the type double expected by parameter $content of TYPO3\CMS\Frontend\Conte...enderer::numberFormat(). ( Ignorable by Annotation )

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

2317
        return $this->numberFormat(/** @scrutinizer ignore-type */ $content, $conf['numberFormat.'] ?? []);
Loading history...
2318
    }
2319
2320
    /**
2321
     * expandList
2322
     * Will return a formatted number based on configuration given as stdWrap properties
2323
     *
2324
     * @param string $content Input value undergoing processing in this function.
2325
     * @return string The processed input value
2326
     */
2327
    public function stdWrap_expandList($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_expandList" is not in camel caps format
Loading history...
2328
    {
2329
        return GeneralUtility::expandList($content);
2330
    }
2331
2332
    /**
2333
     * date
2334
     * Will return a formatted date based on configuration given according to PHP date/gmdate properties
2335
     * Will return gmdate when the property GMT returns TRUE
2336
     *
2337
     * @param string $content Input value undergoing processing in this function.
2338
     * @param array $conf stdWrap properties for date.
2339
     * @return string The processed input value
2340
     */
2341
    public function stdWrap_date($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_date" is not in camel caps format
Loading history...
2342
    {
2343
        // Check for zero length string to mimic default case of date/gmdate.
2344
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
2345
        $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
2346
        return $content;
2347
    }
2348
2349
    /**
2350
     * strftime
2351
     * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties
2352
     * Will return gmstrftime when the property GMT returns TRUE
2353
     *
2354
     * @param string $content Input value undergoing processing in this function.
2355
     * @param array $conf stdWrap properties for strftime.
2356
     * @return string The processed input value
2357
     */
2358
    public function stdWrap_strftime($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_strftime" is not in camel caps format
Loading history...
2359
    {
2360
        // Check for zero length string to mimic default case of strtime/gmstrftime
2361
        $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content;
2362
        $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT'])
2363
            ? gmstrftime($conf['strftime'] ?? null, $content)
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2364
            : strftime($conf['strftime'] ?? null, $content);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2365
        if (!empty($conf['strftime.']['charset'])) {
2366
            $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['strftime.']['charset'])));
2367
            return $output ?: $content;
2368
        }
2369
        return $content;
2370
    }
2371
2372
    /**
2373
     * strtotime
2374
     * Will return a timestamp based on configuration given according to PHP strtotime
2375
     *
2376
     * @param string $content Input value undergoing processing in this function.
2377
     * @param array $conf stdWrap properties for strtotime.
2378
     * @return string The processed input value
2379
     */
2380
    public function stdWrap_strtotime($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_strtotime" is not in camel caps format
Loading history...
2381
    {
2382
        if ($conf['strtotime'] !== '1') {
2383
            $content .= ' ' . $conf['strtotime'];
2384
        }
2385
        return strtotime($content, $GLOBALS['EXEC_TIME']);
2386
    }
2387
2388
    /**
2389
     * age
2390
     * Will return the age of a given timestamp based on configuration given by stdWrap properties
2391
     *
2392
     * @param string $content Input value undergoing processing in this function.
2393
     * @param array $conf stdWrap properties for age.
2394
     * @return string The processed input value
2395
     */
2396
    public function stdWrap_age($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_age" is not in camel caps format
Loading history...
2397
    {
2398
        return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null);
2399
    }
2400
2401
    /**
2402
     * case
2403
     * Will transform the content to be upper or lower case only
2404
     * Leaves HTML tags untouched
2405
     *
2406
     * @param string $content Input value undergoing processing in this function.
2407
     * @param array $conf stdWrap properties for case.
2408
     * @return string The processed input value
2409
     */
2410
    public function stdWrap_case($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_case" is not in camel caps format
Loading history...
2411
    {
2412
        return $this->HTMLcaseshift($content, $conf['case']);
2413
    }
2414
2415
    /**
2416
     * bytes
2417
     * Will return the size of a given number in Bytes	 *
2418
     *
2419
     * @param string $content Input value undergoing processing in this function.
2420
     * @param array $conf stdWrap properties for bytes.
2421
     * @return string The processed input value
2422
     */
2423
    public function stdWrap_bytes($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_bytes" is not in camel caps format
Loading history...
2424
    {
2425
        return GeneralUtility::formatSize($content, $conf['bytes.']['labels'], $conf['bytes.']['base']);
0 ignored issues
show
Bug introduced by
$content of type string is incompatible with the type integer expected by parameter $sizeInBytes of TYPO3\CMS\Core\Utility\G...alUtility::formatSize(). ( Ignorable by Annotation )

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

2425
        return GeneralUtility::formatSize(/** @scrutinizer ignore-type */ $content, $conf['bytes.']['labels'], $conf['bytes.']['base']);
Loading history...
2426
    }
2427
2428
    /**
2429
     * substring
2430
     * Will return a substring based on position information given by stdWrap properties
2431
     *
2432
     * @param string $content Input value undergoing processing in this function.
2433
     * @param array $conf stdWrap properties for substring.
2434
     * @return string The processed input value
2435
     */
2436
    public function stdWrap_substring($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_substring" is not in camel caps format
Loading history...
2437
    {
2438
        return $this->substring($content, $conf['substring']);
2439
    }
2440
2441
    /**
2442
     * cropHTML
2443
     * Crops content to a given size while leaving HTML tags untouched
2444
     *
2445
     * @param string $content Input value undergoing processing in this function.
2446
     * @param array $conf stdWrap properties for cropHTML.
2447
     * @return string The processed input value
2448
     */
2449
    public function stdWrap_cropHTML($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_cropHTML" is not in camel caps format
Loading history...
2450
    {
2451
        return $this->cropHTML($content, $conf['cropHTML'] ?? '');
2452
    }
2453
2454
    /**
2455
     * stripHtml
2456
     * Completely removes HTML tags from content
2457
     *
2458
     * @param string $content Input value undergoing processing in this function.
2459
     * @return string The processed input value
2460
     */
2461
    public function stdWrap_stripHtml($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_stripHtml" is not in camel caps format
Loading history...
2462
    {
2463
        return strip_tags($content);
2464
    }
2465
2466
    /**
2467
     * crop
2468
     * Crops content to a given size without caring about HTML tags
2469
     *
2470
     * @param string $content Input value undergoing processing in this function.
2471
     * @param array $conf stdWrap properties for crop.
2472
     * @return string The processed input value
2473
     */
2474
    public function stdWrap_crop($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_crop" is not in camel caps format
Loading history...
2475
    {
2476
        return $this->crop($content, $conf['crop']);
2477
    }
2478
2479
    /**
2480
     * rawUrlEncode
2481
     * Encodes content to be used within URLs
2482
     *
2483
     * @param string $content Input value undergoing processing in this function.
2484
     * @return string The processed input value
2485
     */
2486
    public function stdWrap_rawUrlEncode($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_rawUrlEncode" is not in camel caps format
Loading history...
2487
    {
2488
        return rawurlencode($content);
2489
    }
2490
2491
    /**
2492
     * htmlSpecialChars
2493
     * Transforms HTML tags to readable text by replacing special characters with their HTML entity
2494
     * When preserveEntities returns TRUE, existing entities will be left untouched
2495
     *
2496
     * @param string $content Input value undergoing processing in this function.
2497
     * @param array $conf stdWrap properties for htmlSpecialChars.
2498
     * @return string The processed input value
2499
     */
2500
    public function stdWrap_htmlSpecialChars($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_htmlSpecialChars" is not in camel caps format
Loading history...
2501
    {
2502
        if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) {
2503
            $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false);
2504
        } else {
2505
            $content = htmlspecialchars($content);
2506
        }
2507
        return $content;
2508
    }
2509
2510
    /**
2511
     * encodeForJavaScriptValue
2512
     * Escapes content to be used inside JavaScript strings. Single quotes are added around the value.
2513
     *
2514
     * @param string $content Input value undergoing processing in this function
2515
     * @return string The processed input value
2516
     */
2517
    public function stdWrap_encodeForJavaScriptValue($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_encodeForJavaScriptValue" is not in camel caps format
Loading history...
2518
    {
2519
        return GeneralUtility::quoteJSvalue($content);
2520
    }
2521
2522
    /**
2523
     * doubleBrTag
2524
     * Searches for double line breaks and replaces them with the given value
2525
     *
2526
     * @param string $content Input value undergoing processing in this function.
2527
     * @param array $conf stdWrap properties for doubleBrTag.
2528
     * @return string The processed input value
2529
     */
2530
    public function stdWrap_doubleBrTag($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_doubleBrTag" is not in camel caps format
Loading history...
2531
    {
2532
        return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? null, $content);
2533
    }
2534
2535
    /**
2536
     * br
2537
     * Searches for single line breaks and replaces them with a <br />/<br> tag
2538
     * according to the doctype
2539
     *
2540
     * @param string $content Input value undergoing processing in this function.
2541
     * @return string The processed input value
2542
     */
2543
    public function stdWrap_br($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_br" is not in camel caps format
Loading history...
2544
    {
2545
        return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype));
2546
    }
2547
2548
    /**
2549
     * brTag
2550
     * Searches for single line feeds and replaces them with the given value
2551
     *
2552
     * @param string $content Input value undergoing processing in this function.
2553
     * @param array $conf stdWrap properties for brTag.
2554
     * @return string The processed input value
2555
     */
2556
    public function stdWrap_brTag($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_brTag" is not in camel caps format
Loading history...
2557
    {
2558
        return str_replace(LF, $conf['brTag'] ?? null, $content);
2559
    }
2560
2561
    /**
2562
     * encapsLines
2563
     * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet
2564
     * and wrapping them with values given by stdWrap properties
2565
     *
2566
     * @param string $content Input value undergoing processing in this function.
2567
     * @param array $conf stdWrap properties for erncapsLines.
2568
     * @return string The processed input value
2569
     */
2570
    public function stdWrap_encapsLines($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_encapsLines" is not in camel caps format
Loading history...
2571
    {
2572
        return $this->encaps_lineSplit($content, $conf['encapsLines.']);
2573
    }
2574
2575
    /**
2576
     * keywords
2577
     * Transforms content into a CSV list to be used i.e. as keywords within a meta tag
2578
     *
2579
     * @param string $content Input value undergoing processing in this function.
2580
     * @return string The processed input value
2581
     */
2582
    public function stdWrap_keywords($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_keywords" is not in camel caps format
Loading history...
2583
    {
2584
        return $this->keywords($content);
2585
    }
2586
2587
    /**
2588
     * innerWrap
2589
     * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2590
     * See wrap
2591
     *
2592
     * @param string $content Input value undergoing processing in this function.
2593
     * @param array $conf stdWrap properties for innerWrap.
2594
     * @return string The processed input value
2595
     */
2596
    public function stdWrap_innerWrap($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_innerWrap" is not in camel caps format
Loading history...
2597
    {
2598
        return $this->wrap($content, $conf['innerWrap'] ?? null);
2599
    }
2600
2601
    /**
2602
     * innerWrap2
2603
     * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content
2604
     * See wrap
2605
     *
2606
     * @param string $content Input value undergoing processing in this function.
2607
     * @param array $conf stdWrap properties for innerWrap2.
2608
     * @return string The processed input value
2609
     */
2610
    public function stdWrap_innerWrap2($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_innerWrap2" is not in camel caps format
Loading history...
2611
    {
2612
        return $this->wrap($content, $conf['innerWrap2'] ?? null);
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_preCObject" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_postCObject" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_wrapAlign" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_typolink" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_wrap" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_noTrimWrap" is not in camel caps format
Loading history...
2705
    {
2706
        $splitChar = isset($conf['noTrimWrap.']['splitChar.'])
2707
            ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2708
            : $conf['noTrimWrap.']['splitChar'] ?? '';
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_wrap2" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_dataWrap" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_prepend" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_append" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_wrap3" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_orderedStdWrap" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_outerWrap" is not in camel caps format
Loading history...
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 = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_insertData" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_postUserFunc" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_postUserFuncInt" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_prefixComment" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_editIcons" is not in camel caps format
Loading history...
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 = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_editPanel" is not in camel caps format
Loading history...
2921
    {
2922
        if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
2923
            $content = $this->editPanel($content, $conf['editPanel.']);
2924
        }
2925
        return $content;
2926
    }
2927
2928
    /**
2929
     * Store content into cache
2930
     *
2931
     * @param string $content Input value undergoing processing in these functions.
2932
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2933
     * @return string The processed input value
2934
     */
2935
    public function stdWrap_cacheStore($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_cacheStore" is not in camel caps format
Loading history...
2936
    {
2937
        if (!isset($conf['cache.'])) {
2938
            return $content;
2939
        }
2940
        $key = $this->calculateCacheKey($conf['cache.']);
2941
        if (empty($key)) {
2942
            return $content;
2943
        }
2944
        /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
2945
        $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
2946
        $tags = $this->calculateCacheTags($conf['cache.']);
2947
        $lifetime = $this->calculateCacheLifetime($conf['cache.']);
2948
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) {
2949
            $params = [
2950
                'key' => $key,
2951
                'content' => $content,
2952
                'lifetime' => $lifetime,
2953
                'tags' => $tags
2954
            ];
2955
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2956
            GeneralUtility::callUserFunction($_funcRef, $params, $ref);
2957
        }
2958
        $cacheFrontend->set($key, $content, $tags, $lifetime);
2959
        return $content;
2960
    }
2961
2962
    /**
2963
     * stdWrap post process hook
2964
     * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs
2965
     * this hook executes functions at after the content has been modified by the rest of the stdWrap functions but still before debugging
2966
     *
2967
     * @param string $content Input value undergoing processing in these functions.
2968
     * @param array $conf All stdWrap properties, not just the ones for a particular function.
2969
     * @return string The processed input value
2970
     */
2971
    public function stdWrap_stdWrapPostProcess($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_stdWrapPostProcess" is not in camel caps format
Loading history...
2972
    {
2973
        foreach ($this->stdWrapHookObjects as $hookObject) {
2974
            /** @var ContentObjectStdWrapHookInterface $hookObject */
2975
            $content = $hookObject->stdWrapPostProcess($content, $conf, $this);
2976
        }
2977
        return $content;
2978
    }
2979
2980
    /**
2981
     * debug
2982
     * Will output the content as readable HTML code
2983
     *
2984
     * @param string $content Input value undergoing processing in this function.
2985
     * @return string The processed input value
2986
     */
2987
    public function stdWrap_debug($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_debug" is not in camel caps format
Loading history...
2988
    {
2989
        return '<pre>' . htmlspecialchars($content) . '</pre>';
2990
    }
2991
2992
    /**
2993
     * debugFunc
2994
     * Will output the content in a debug table
2995
     *
2996
     * @param string $content Input value undergoing processing in this function.
2997
     * @param array $conf stdWrap properties for debugFunc.
2998
     * @return string The processed input value
2999
     */
3000
    public function stdWrap_debugFunc($content = '', $conf = [])
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_debugFunc" is not in camel caps format
Loading history...
3001
    {
3002
        debug((int)$conf['debugFunc'] === 2 ? [$content] : $content);
3003
        return $content;
3004
    }
3005
3006
    /**
3007
     * debugData
3008
     * Will output the data used by the current record in a debug table
3009
     *
3010
     * @param string $content Input value undergoing processing in this function.
3011
     * @return string The processed input value
3012
     */
3013
    public function stdWrap_debugData($content = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::stdWrap_debugData" is not in camel caps format
Loading history...
3014
    {
3015
        debug($this->data, '$cObj->data:');
3016
        if (is_array($this->alternativeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->alternativeData) is always false.
Loading history...
3017
            debug($this->alternativeData, '$this->alternativeData');
3018
        }
3019
        return $content;
3020
    }
3021
3022
    /**
3023
     * Returns number of rows selected by the query made by the properties set.
3024
     * Implements the stdWrap "numRows" property
3025
     *
3026
     * @param array $conf TypoScript properties for the property (see link to "numRows")
3027
     * @return int The number of rows found by the select
3028
     * @internal
3029
     * @see stdWrap()
3030
     */
3031
    public function numRows($conf)
3032
    {
3033
        $conf['select.']['selectFields'] = 'count(*)';
3034
        $statement = $this->exec_getQuery($conf['table'], $conf['select.']);
3035
3036
        return (int)$statement->fetchColumn(0);
3037
    }
3038
3039
    /**
3040
     * Exploding a string by the $char value (if integer its an ASCII value) and returning index $listNum
3041
     *
3042
     * @param string $content String to explode
3043
     * @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())
3044
     * @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.
3045
     * @return string
3046
     */
3047
    public function listNum($content, $listNum, $char)
3048
    {
3049
        $char = $char ?: ',';
3050
        if (MathUtility::canBeInterpretedAsInteger($char)) {
3051
            $char = chr($char);
0 ignored issues
show
Bug introduced by
$char of type string is incompatible with the type integer expected by parameter $ascii of chr(). ( Ignorable by Annotation )

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

3051
            $char = chr(/** @scrutinizer ignore-type */ $char);
Loading history...
3052
        }
3053
        $temp = explode($char, $content);
3054
        $last = '' . (count($temp) - 1);
3055
        // Take a random item if requested
3056
        if ($listNum === 'rand') {
3057
            $listNum = random_int(0, count($temp) - 1);
3058
        }
3059
        $index = $this->calc(str_ireplace('last', $last, $listNum));
3060
        return $temp[$index];
3061
    }
3062
3063
    /**
3064
     * Compares values together based on the settings in the input TypoScript array and returns the comparison result.
3065
     * Implements the "if" function in TYPO3 TypoScript
3066
     *
3067
     * @param array $conf TypoScript properties defining what to compare
3068
     * @return bool
3069
     * @see stdWrap()
3070
     * @see _parseFunc()
3071
     */
3072
    public function checkIf($conf)
3073
    {
3074
        if (!is_array($conf)) {
0 ignored issues
show
introduced by
The condition is_array($conf) is always true.
Loading history...
3075
            return true;
3076
        }
3077
        if (isset($conf['directReturn'])) {
3078
            return (bool)$conf['directReturn'];
3079
        }
3080
        $flag = true;
3081
        if (isset($conf['isNull.'])) {
3082
            $isNull = $this->stdWrap('', $conf['isNull.']);
3083
            if ($isNull !== null) {
0 ignored issues
show
introduced by
The condition $isNull !== null is always true.
Loading history...
3084
                $flag = false;
3085
            }
3086
        }
3087
        if (isset($conf['isTrue']) || isset($conf['isTrue.'])) {
3088
            $isTrue = isset($conf['isTrue.']) ? trim($this->stdWrap($conf['isTrue'], $conf['isTrue.'])) : trim($conf['isTrue']);
3089
            if (!$isTrue) {
3090
                $flag = false;
3091
            }
3092
        }
3093
        if (isset($conf['isFalse']) || isset($conf['isFalse.'])) {
3094
            $isFalse = isset($conf['isFalse.']) ? trim($this->stdWrap($conf['isFalse'], $conf['isFalse.'])) : trim($conf['isFalse']);
3095
            if ($isFalse) {
3096
                $flag = false;
3097
            }
3098
        }
3099
        if (isset($conf['isPositive']) || isset($conf['isPositive.'])) {
3100
            $number = isset($conf['isPositive.']) ? $this->calc($this->stdWrap($conf['isPositive'], $conf['isPositive.'])) : $this->calc($conf['isPositive']);
3101
            if ($number < 1) {
3102
                $flag = false;
3103
            }
3104
        }
3105
        if ($flag) {
3106
            $value = isset($conf['value.'])
3107
                ? trim($this->stdWrap($conf['value'] ?? '', $conf['value.']))
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3108
                : trim($conf['value'] ?? '');
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3109
            if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) {
3110
                $number = isset($conf['isGreaterThan.']) ? trim($this->stdWrap($conf['isGreaterThan'], $conf['isGreaterThan.'])) : trim($conf['isGreaterThan']);
3111
                if ($number <= $value) {
3112
                    $flag = false;
3113
                }
3114
            }
3115
            if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) {
3116
                $number = isset($conf['isLessThan.']) ? trim($this->stdWrap($conf['isLessThan'], $conf['isLessThan.'])) : trim($conf['isLessThan']);
3117
                if ($number >= $value) {
3118
                    $flag = false;
3119
                }
3120
            }
3121
            if (isset($conf['equals']) || isset($conf['equals.'])) {
3122
                $number = isset($conf['equals.']) ? trim($this->stdWrap($conf['equals'], $conf['equals.'])) : trim($conf['equals']);
3123
                if ($number != $value) {
3124
                    $flag = false;
3125
                }
3126
            }
3127
            if (isset($conf['isInList']) || isset($conf['isInList.'])) {
3128
                $number = isset($conf['isInList.']) ? trim($this->stdWrap($conf['isInList'], $conf['isInList.'])) : trim($conf['isInList']);
3129
                if (!GeneralUtility::inList($value, $number)) {
3130
                    $flag = false;
3131
                }
3132
            }
3133
            if (isset($conf['bitAnd']) || isset($conf['bitAnd.'])) {
3134
                $number = isset($conf['bitAnd.']) ? trim($this->stdWrap($conf['bitAnd'], $conf['bitAnd.'])) : trim($conf['bitAnd']);
3135
                if ((new BitSet($number))->get($value) === false) {
0 ignored issues
show
Bug introduced by
$number of type string is incompatible with the type integer expected by parameter $set of TYPO3\CMS\Core\Type\BitSet::__construct(). ( Ignorable by Annotation )

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

3135
                if ((new BitSet(/** @scrutinizer ignore-type */ $number))->get($value) === false) {
Loading history...
Bug introduced by
$value of type string is incompatible with the type integer expected by parameter $bitIndex of TYPO3\CMS\Core\Type\BitSet::get(). ( Ignorable by Annotation )

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

3135
                if ((new BitSet($number))->get(/** @scrutinizer ignore-type */ $value) === false) {
Loading history...
3136
                    $flag = false;
3137
                }
3138
            }
3139
        }
3140
        if ($conf['negate'] ?? false) {
3141
            $flag = !$flag;
3142
        }
3143
        return $flag;
3144
    }
3145
3146
    /**
3147
     * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser"
3148
     * together with the TypoScript options which are first converted from a TS style array
3149
     * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class.
3150
     *
3151
     * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser
3152
     * @param array $conf TypoScript properties for the parser. See link.
3153
     * @return string Return value.
3154
     * @see stdWrap()
3155
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLparserConfig()
3156
     * @see \TYPO3\CMS\Core\Html\HtmlParser::HTMLcleaner()
3157
     */
3158
    public function HTMLparser_TSbridge($theValue, $conf)
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::HTMLparser_TSbridge" is not in camel caps format
Loading history...
3159
    {
3160
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3161
        $htmlParserCfg = $htmlParser->HTMLparserConfig($conf);
3162
        return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]);
3163
    }
3164
3165
    /**
3166
     * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes.
3167
     *
3168
     * @param string $content Input string being wrapped
3169
     * @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.
3170
     * @return string Output string wrapped in the wrapping value.
3171
     * @see insertData()
3172
     * @see stdWrap()
3173
     */
3174
    public function dataWrap($content, $wrap)
3175
    {
3176
        return $this->wrap($content, $this->insertData($wrap));
3177
    }
3178
3179
    /**
3180
     * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they
3181
     * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces.
3182
     * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine
3183
     * DBAL and is skipped here for later processing.
3184
     *
3185
     * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with
3186
     * the current pages title field value.
3187
     *
3188
     * @param string $str Input value
3189
     * @return string Processed input value
3190
     * @see getData()
3191
     * @see stdWrap()
3192
     * @see dataWrap()
3193
     */
3194
    public function insertData($str)
3195
    {
3196
        $inside = 0;
3197
        $newVal = '';
3198
        $pointer = 0;
3199
        $totalLen = strlen($str);
3200
        do {
3201
            if (!$inside) {
3202
                $len = strcspn(substr($str, $pointer), '{');
3203
                $newVal .= substr($str, $pointer, $len);
3204
                $inside = true;
3205
                if (substr($str, $pointer + $len + 1, 1) === '#') {
3206
                    $len2 = strcspn(substr($str, $pointer + $len), '}');
3207
                    $newVal .= substr($str, $pointer + $len, $len2);
3208
                    $len += $len2;
3209
                    $inside = false;
3210
                }
3211
            } else {
3212
                $len = strcspn(substr($str, $pointer), '}') + 1;
3213
                $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data);
3214
                $inside = false;
3215
            }
3216
            $pointer += $len;
3217
        } while ($pointer < $totalLen);
3218
        return $newVal;
3219
    }
3220
3221
    /**
3222
     * 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.
3223
     * Notice; this function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript.
3224
     *
3225
     * @param string $str Input value
3226
     * @param array $conf TypoScript Configuration (not used at this point.)
3227
     * @param string $content The content to wrap the comment around.
3228
     * @return string Processed input value
3229
     * @see stdWrap()
3230
     */
3231
    public function prefixComment($str, $conf, $content)
3232
    {
3233
        if (empty($str)) {
3234
            return $content;
3235
        }
3236
        $parts = explode('|', $str);
3237
        $indent = (int)$parts[0];
3238
        $comment = htmlspecialchars($this->insertData($parts[1]));
3239
        $output = LF
3240
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF
3241
            . str_pad('', $indent + 1, "\t") . $content . LF
3242
            . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF
3243
            . str_pad('', $indent + 1, "\t");
3244
        return $output;
3245
    }
3246
3247
    /**
3248
     * Implements the stdWrap property "substring" which is basically a TypoScript implementation of the PHP function, substr()
3249
     *
3250
     * @param string $content The string to perform the operation on
3251
     * @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().
3252
     * @return string The processed input value.
3253
     * @internal
3254
     * @see stdWrap()
3255
     */
3256
    public function substring($content, $options)
3257
    {
3258
        $options = GeneralUtility::intExplode(',', $options . ',');
3259
        if ($options[1]) {
3260
            return mb_substr($content, $options[0], $options[1], 'utf-8');
0 ignored issues
show
Bug introduced by
$options[1] of type string is incompatible with the type integer expected by parameter $length of mb_substr(). ( Ignorable by Annotation )

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

3260
            return mb_substr($content, $options[0], /** @scrutinizer ignore-type */ $options[1], 'utf-8');
Loading history...
Bug introduced by
$options[0] of type string is incompatible with the type integer expected by parameter $start of mb_substr(). ( Ignorable by Annotation )

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

3260
            return mb_substr($content, /** @scrutinizer ignore-type */ $options[0], $options[1], 'utf-8');
Loading history...
3261
        }
3262
        return mb_substr($content, $options[0], null, 'utf-8');
3263
    }
3264
3265
    /**
3266
     * 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.
3267
     *
3268
     * @param string $content The string to perform the operation on
3269
     * @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.
3270
     * @return string The processed input value.
3271
     * @internal
3272
     * @see stdWrap()
3273
     */
3274
    public function crop($content, $options)
3275
    {
3276
        $options = explode('|', $options);
3277
        $chars = (int)$options[0];
3278
        $afterstring = trim($options[1] ?? '');
3279
        $crop2space = trim($options[2] ?? '');
3280
        if ($chars) {
3281
            if (mb_strlen($content, 'utf-8') > abs($chars)) {
3282
                $truncatePosition = false;
3283
                if ($chars < 0) {
3284
                    $content = mb_substr($content, $chars, null, 'utf-8');
3285
                    if ($crop2space) {
3286
                        $truncatePosition = strpos($content, ' ');
3287
                    }
3288
                    $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content;
3289
                } else {
3290
                    $content = mb_substr($content, 0, $chars, 'utf-8');
3291
                    if ($crop2space) {
3292
                        $truncatePosition = strrpos($content, ' ');
3293
                    }
3294
                    $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring;
3295
                }
3296
            }
3297
        }
3298
        return $content;
3299
    }
3300
3301
    /**
3302
     * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length
3303
     * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string
3304
     * really was cropped.
3305
     *
3306
     * Compared to stdWrap.crop it respects HTML tags and entities.
3307
     *
3308
     * @param string $content The string to perform the operation on
3309
     * @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.
3310
     * @return string The processed input value.
3311
     * @internal
3312
     * @see stdWrap()
3313
     */
3314
    public function cropHTML($content, $options)
3315
    {
3316
        $options = explode('|', $options);
3317
        $chars = (int)$options[0];
3318
        $absChars = abs($chars);
3319
        $replacementForEllipsis = trim($options[1] ?? '');
3320
        $crop2space = trim($options[2] ?? '') === '1';
3321
        // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
3322
        $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';
3323
        $tagsRegEx = '
3324
			(
3325
				(?:
3326
					<!--.*?-->					# a comment
3327
					|
3328
					<canvas[^>]*>.*?</canvas>   # a canvas tag
3329
					|
3330
					<script[^>]*>.*?</script>   # a script tag
3331
					|
3332
					<noscript[^>]*>.*?</noscript> # a noscript tag
3333
					|
3334
					<template[^>]*>.*?</template> # a template tag
3335
				)
3336
				|
3337
				</?(?:' . $tags . ')+			# opening tag (\'<tag\') or closing tag (\'</tag\')
3338
				(?:
3339
					(?:
3340
						(?:
3341
							\\s+\\w[\\w-]*		# EITHER spaces, followed by attribute names
3342
							(?:
3343
								\\s*=?\\s*		# equals
3344
								(?>
3345
									".*?"		# attribute values in double-quotes
3346
									|
3347
									\'.*?\'		# attribute values in single-quotes
3348
									|
3349
									[^\'">\\s]+	# plain attribute values
3350
								)
3351
							)?
3352
						)
3353
						|						# OR a single dash (for TYPO3 link tag)
3354
						(?:
3355
							\\s+-
3356
						)
3357
					)+\\s*
3358
					|							# OR only spaces
3359
					\\s*
3360
				)
3361
				/?>								# closing the tag with \'>\' or \'/>\'
3362
			)';
3363
        $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
3364
        // Reverse array if we are cropping from right.
3365
        if ($chars < 0) {
3366
            $splittedContent = array_reverse($splittedContent);
0 ignored issues
show
Bug introduced by
It seems like $splittedContent can also be of type false; however, parameter $array of array_reverse() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

3366
            $splittedContent = array_reverse(/** @scrutinizer ignore-type */ $splittedContent);
Loading history...
3367
        }
3368
        // Crop the text (chars of tag-blocks are not counted).
3369
        $strLen = 0;
3370
        // This is the offset of the content item which was cropped.
3371
        $croppedOffset = null;
3372
        $countSplittedContent = count($splittedContent);
0 ignored issues
show
Bug introduced by
It seems like $splittedContent can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

3372
        $countSplittedContent = count(/** @scrutinizer ignore-type */ $splittedContent);
Loading history...
3373
        for ($offset = 0; $offset < $countSplittedContent; $offset++) {
3374
            if ($offset % 2 === 0) {
3375
                $tempContent = $splittedContent[$offset];
3376
                $thisStrLen = mb_strlen(html_entity_decode($tempContent, ENT_COMPAT, 'UTF-8'), 'utf-8');
3377
                if ($strLen + $thisStrLen > $absChars) {
3378
                    $croppedOffset = $offset;
3379
                    $cropPosition = $absChars - $strLen;
3380
                    // The snippet "&[^&\s;]{2,8};" in the RegEx below represents entities.
3381
                    $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)';
3382
                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}#uis';
3383
                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3384
                        $tempContentPlusOneCharacter = $croppedMatch[0];
3385
                    } else {
3386
                        $tempContentPlusOneCharacter = false;
3387
                    }
3388
                    $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}#uis';
3389
                    if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3390
                        $tempContent = $croppedMatch[0];
3391
                        if ($crop2space && $tempContentPlusOneCharacter !== false) {
3392
                            $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}(?=\\s)#uis';
3393
                            if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) {
3394
                                $tempContent = $croppedMatch[0];
3395
                            }
3396
                        }
3397
                    }
3398
                    $splittedContent[$offset] = $tempContent;
3399
                    break;
3400
                }
3401
                $strLen += $thisStrLen;
3402
            }
3403
        }
3404
        // Close cropped tags.
3405
        $closingTags = [];
3406
        if ($croppedOffset !== null) {
3407
            $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
3408
            $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
3409
            for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
3410
                if (substr($splittedContent[$offset], -2) === '/>') {
3411
                    // Ignore empty element tags (e.g. <br />).
3412
                    continue;
3413
                }
3414
                preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches);
3415
                $tagName = $matches[1] ?? null;
3416
                if ($tagName !== null) {
3417
                    // Seek for the closing (or opening) tag.
3418
                    $countSplittedContent = count($splittedContent);
3419
                    for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) {
3420
                        preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches);
3421
                        $seekingTagName = $matches[1] ?? null;
3422
                        if ($tagName === $seekingTagName) {
3423
                            // We found a matching tag.
3424
                            // Add closing tag only if it occurs after the cropped content item.
3425
                            if ($seekingOffset > $croppedOffset) {
3426
                                $closingTags[] = $splittedContent[$seekingOffset];
3427
                            }
3428
                            break;
3429
                        }
3430
                    }
3431
                }
3432
            }
3433
            // Drop the cropped items of the content array. The $closingTags will be added later on again.
3434
            array_splice($splittedContent, $croppedOffset + 1);
0 ignored issues
show
Bug introduced by
It seems like $splittedContent can also be of type false; however, parameter $input of array_splice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

3434
            array_splice(/** @scrutinizer ignore-type */ $splittedContent, $croppedOffset + 1);
Loading history...
3435
        }
3436
        $splittedContent = array_merge($splittedContent, [
3437
            $croppedOffset !== null ? $replacementForEllipsis : ''
3438
        ], $closingTags);
3439
        // Reverse array once again if we are cropping from the end.
3440
        if ($chars < 0) {
3441
            $splittedContent = array_reverse($splittedContent);
3442
        }
3443
        return implode('', $splittedContent);
3444
    }
3445
3446
    /**
3447
     * Performs basic mathematical evaluation of the input string. Does NOT take parenthesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction())
3448
     *
3449
     * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
3450
     * @return int The result (might be a float if you did a division of the numbers).
3451
     * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()
3452
     */
3453
    public function calc($val)
3454
    {
3455
        $parts = GeneralUtility::splitCalc($val, '+-*/');
3456
        $value = 0;
3457
        foreach ($parts as $part) {
3458
            $theVal = $part[1];
3459
            $sign = $part[0];
3460
            if ((string)(int)$theVal === (string)$theVal) {
3461
                $theVal = (int)$theVal;
3462
            } else {
3463
                $theVal = 0;
3464
            }
3465
            if ($sign === '-') {
3466
                $value -= $theVal;
3467
            }
3468
            if ($sign === '+') {
3469
                $value += $theVal;
3470
            }
3471
            if ($sign === '/') {
3472
                if ((int)$theVal) {
3473
                    $value /= (int)$theVal;
3474
                }
3475
            }
3476
            if ($sign === '*') {
3477
                $value *= $theVal;
3478
            }
3479
        }
3480
        return $value;
3481
    }
3482
3483
    /**
3484
     * 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.
3485
     * 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.
3486
     * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse.
3487
     *
3488
     * @param string $value The string value to explode by $conf[token] and process each part
3489
     * @param array $conf TypoScript properties for "split
3490
     * @return string Compiled result
3491
     * @internal
3492
     * @see stdWrap()
3493
     * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::processItemStates()
3494
     */
3495
    public function splitObj($value, $conf)
3496
    {
3497
        $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'], $conf['token.']) : $conf['token'];
3498
        if ($conf['token'] === '') {
3499
            return $value;
3500
        }
3501
        $valArr = explode($conf['token'], $value);
3502
3503
        // return value directly by returnKey. No further processing
3504
        if (!empty($valArr) && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))
3505
        ) {
3506
            $key = isset($conf['returnKey.']) ? (int)$this->stdWrap($conf['returnKey'], $conf['returnKey.']) : (int)$conf['returnKey'];
3507
            return $valArr[$key] ?? '';
3508
        }
3509
3510
        // return the amount of elements. No further processing
3511
        if (!empty($valArr) && ($conf['returnCount'] || $conf['returnCount.'])) {
3512
            $returnCount = isset($conf['returnCount.']) ? (bool)$this->stdWrap($conf['returnCount'], $conf['returnCount.']) : (bool)$conf['returnCount'];
3513
            return $returnCount ? count($valArr) : 0;
3514
        }
3515
3516
        // calculate splitCount
3517
        $splitCount = count($valArr);
3518
        $max = isset($conf['max.']) ? (int)$this->stdWrap($conf['max'], $conf['max.']) : (int)$conf['max'];
3519
        if ($max && $splitCount > $max) {
3520
            $splitCount = $max;
3521
        }
3522
        $min = isset($conf['min.']) ? (int)$this->stdWrap($conf['min'], $conf['min.']) : (int)$conf['min'];
3523
        if ($min && $splitCount < $min) {
3524
            $splitCount = $min;
3525
        }
3526
        $wrap = isset($conf['wrap.']) ? (string)$this->stdWrap($conf['wrap'], $conf['wrap.']) : (string)$conf['wrap'];
3527
        $cObjNumSplitConf = isset($conf['cObjNum.']) ? (string)$this->stdWrap($conf['cObjNum'], $conf['cObjNum.']) : (string)$conf['cObjNum'];
3528
        $splitArr = [];
3529
        if ($wrap !== '' || $cObjNumSplitConf !== '') {
3530
            $splitArr['wrap'] = $wrap;
3531
            $splitArr['cObjNum'] = $cObjNumSplitConf;
3532
            $splitArr = GeneralUtility::makeInstance(TypoScriptService::class)
3533
                ->explodeConfigurationForOptionSplit($splitArr, $splitCount);
3534
        }
3535
        $content = '';
3536
        for ($a = 0; $a < $splitCount; $a++) {
3537
            $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a;
3538
            $value = '' . $valArr[$a];
3539
            $this->data[$this->currentValKey] = $value;
3540
            if ($splitArr[$a]['cObjNum']) {
3541
                $objName = (int)$splitArr[$a]['cObjNum'];
3542
                $value = isset($conf[$objName . '.'])
3543
                    ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3544
                    : $this->cObjGet($conf[$objName . '.'], $objName . '.');
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3545
            }
3546
            $wrap = isset($splitArr[$a]['wrap.']) ? $this->stdWrap($splitArr[$a]['wrap'], $splitArr[$a]['wrap.']) : $splitArr[$a]['wrap'];
3547
            if ($wrap) {
3548
                $value = $this->wrap($value, $wrap);
3549
            }
3550
            $content .= $value;
3551
        }
3552
        return $content;
3553
    }
3554
3555
    /**
3556
     * Processes ordered replacements on content data.
3557
     *
3558
     * @param string $content The content to be processed
3559
     * @param array $configuration The TypoScript configuration for stdWrap.replacement
3560
     * @return string The processed content data
3561
     */
3562
    protected function replacement($content, array $configuration)
3563
    {
3564
        // Sorts actions in configuration by numeric index
3565
        ksort($configuration, SORT_NUMERIC);
3566
        foreach ($configuration as $index => $action) {
3567
            // Checks whether we have a valid action and a numeric key ending with a dot ("10.")
3568
            if (is_array($action) && substr($index, -1) === '.' && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) {
3569
                $content = $this->replacementSingle($content, $action);
3570
            }
3571
        }
3572
        return $content;
3573
    }
3574
3575
    /**
3576
     * Processes a single search/replace on content data.
3577
     *
3578
     * @param string $content The content to be processed
3579
     * @param array $configuration The TypoScript of the search/replace action to be processed
3580
     * @return string The processed content data
3581
     */
3582
    protected function replacementSingle($content, array $configuration)
3583
    {
3584
        if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) {
3585
            // Gets the strings
3586
            $search = isset($configuration['search.']) ? $this->stdWrap($configuration['search'], $configuration['search.']) : $configuration['search'];
3587
            $replace = isset($configuration['replace.'])
3588
                ? $this->stdWrap($configuration['replace'] ?? null, $configuration['replace.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3589
                : $configuration['replace'] ?? null;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3590
            $useRegularExpression = false;
3591
            // Determines whether regular expression shall be used
3592
            if (isset($configuration['useRegExp'])
3593
                || (isset($configuration['useRegExp.']) && $configuration['useRegExp.'])
3594
            ) {
3595
                $useRegularExpression = isset($configuration['useRegExp.']) ? (bool)$this->stdWrap($configuration['useRegExp'], $configuration['useRegExp.']) : (bool)$configuration['useRegExp'];
3596
            }
3597
            $useOptionSplitReplace = false;
3598
            // Determines whether replace-pattern uses option-split
3599
            if (isset($configuration['useOptionSplitReplace']) || isset($configuration['useOptionSplitReplace.'])) {
3600
                $useOptionSplitReplace = isset($configuration['useOptionSplitReplace.']) ? (bool)$this->stdWrap($configuration['useOptionSplitReplace'], $configuration['useOptionSplitReplace.']) : (bool)$configuration['useOptionSplitReplace'];
3601
            }
3602
3603
            // Performs a replacement by preg_replace()
3604
            if ($useRegularExpression) {
3605
                // Get separator-character which precedes the string and separates search-string from the modifiers
3606
                $separator = $search[0];
3607
                $startModifiers = strrpos($search, $separator);
3608
                if ($separator !== false && $startModifiers > 0) {
3609
                    $modifiers = substr($search, $startModifiers + 1);
3610
                    // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code
3611
                    $modifiers = str_replace('e', '', $modifiers);
3612
                    $search = substr($search, 0, $startModifiers + 1) . $modifiers;
3613
                }
3614
                if ($useOptionSplitReplace) {
3615
                    // init for replacement
3616
                    $splitCount = preg_match_all($search, $content, $matches);
3617
                    $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3618
                    $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3619
                    $replaceCount = 0;
3620
3621
                    $replaceCallback = function ($match) use ($replaceArray, $search, &$replaceCount) {
3622
                        $replaceCount++;
3623
                        return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]);
3624
                    };
3625
                    $content = preg_replace_callback($search, $replaceCallback, $content);
3626
                } else {
3627
                    $content = preg_replace($search, $replace, $content);
3628
                }
3629
            } elseif ($useOptionSplitReplace) {
3630
                // turn search-string into a preg-pattern
3631
                $searchPreg = '#' . preg_quote($search, '#') . '#';
3632
3633
                // init for replacement
3634
                $splitCount = preg_match_all($searchPreg, $content, $matches);
3635
                $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3636
                $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3637
                $replaceCount = 0;
3638
3639
                $replaceCallback = function () use ($replaceArray, &$replaceCount) {
3640
                    $replaceCount++;
3641
                    return $replaceArray[$replaceCount - 1][0];
3642
                };
3643
                $content = preg_replace_callback($searchPreg, $replaceCallback, $content);
3644
            } else {
3645
                $content = str_replace($search, $replace, $content);
3646
            }
3647
        }
3648
        return $content;
3649
    }
3650
3651
    /**
3652
     * Implements the "round" property of stdWrap
3653
     * This is a Wrapper function for PHP's rounding functions (round,ceil,floor), defaults to round()
3654
     *
3655
     * @param string $content Value to process
3656
     * @param array $conf TypoScript configuration for round
3657
     * @return string The formatted number
3658
     */
3659
    protected function round($content, array $conf = [])
3660
    {
3661
        $decimals = isset($conf['decimals.'])
3662
            ? $this->stdWrap($conf['decimals'] ?? '', $conf['decimals.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3663
            : ($conf['decimals'] ?? null);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3664
        $type = isset($conf['roundType.'])
3665
            ? $this->stdWrap($conf['roundType'] ?? '', $conf['roundType.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3666
            : ($conf['roundType'] ?? null);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3667
        $floatVal = (float)$content;
3668
        switch ($type) {
3669
            case 'ceil':
3670
                $content = ceil($floatVal);
3671
                break;
3672
            case 'floor':
3673
                $content = floor($floatVal);
3674
                break;
3675
            case 'round':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
3676
3677
            default:
3678
                $content = round($floatVal, (int)$decimals);
3679
        }
3680
        return $content;
3681
    }
3682
3683
    /**
3684
     * Implements the stdWrap property "numberFormat"
3685
     * This is a Wrapper function for php's number_format()
3686
     *
3687
     * @param float $content Value to process
3688
     * @param array $conf TypoScript Configuration for numberFormat
3689
     * @return string The formatted number
3690
     */
3691
    public function numberFormat($content, $conf)
3692
    {
3693
        $decimals = isset($conf['decimals.'])
3694
            ? (int)$this->stdWrap($conf['decimals'] ?? '', $conf['decimals.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3695
            : (int)($conf['decimals'] ?? 0);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3696
        $dec_point = isset($conf['dec_point.'])
3697
            ? $this->stdWrap($conf['dec_point'] ?? '', $conf['dec_point.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3698
            : ($conf['dec_point'] ?? null);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3699
        $thousands_sep = isset($conf['thousands_sep.'])
3700
            ? $this->stdWrap($conf['thousands_sep'] ?? '', $conf['thousands_sep.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3701
            : ($conf['thousands_sep'] ?? null);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3702
        return number_format((float)$content, $decimals, $dec_point, $thousands_sep);
3703
    }
3704
3705
    /**
3706
     * Implements the stdWrap property, "parseFunc".
3707
     * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text
3708
     * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc.
3709
     * It is still a very important function for processing of bodytext which is normally stored in the database
3710
     * in a format which is not fully ready to be outputted.
3711
     * This situation has not become better by having a RTE around...
3712
     *
3713
     * This function is actually just splitting the input content according to the configuration of "external blocks".
3714
     * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed
3715
     * (while other parts/blocks should NOT be parsed).
3716
     * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc()
3717
     *
3718
     * @param string $theValue The value to process.
3719
     * @param array $conf TypoScript configuration for parseFunc
3720
     * @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!
3721
     * @return string The processed value
3722
     * @see _parseFunc()
3723
     */
3724
    public function parseFunc($theValue, $conf, $ref = '')
3725
    {
3726
        // Fetch / merge reference, if any
3727
        if ($ref) {
3728
            $temp_conf = [
3729
                'parseFunc' => $ref,
3730
                'parseFunc.' => $conf
3731
            ];
3732
            $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc');
3733
            $conf = $temp_conf['parseFunc.'];
3734
        }
3735
        // Process:
3736
        if ((string)($conf['externalBlocks'] ?? '') === '') {
3737
            return $this->_parseFunc($theValue, $conf);
3738
        }
3739
        $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks'])));
3740
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3741
        $parts = $htmlParser->splitIntoBlock($tags, $theValue);
3742
        foreach ($parts as $k => $v) {
3743
            if ($k % 2) {
3744
                // font:
3745
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3746
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3747
                if ($cfg['stripNLprev'] || $cfg['stripNL']) {
3748
                    $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]);
3749
                }
3750
                if ($cfg['stripNLnext'] || $cfg['stripNL']) {
3751
                    $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]);
3752
                }
3753
            }
3754
        }
3755
        foreach ($parts as $k => $v) {
3756
            if ($k % 2) {
3757
                $tag = $htmlParser->getFirstTag($v);
3758
                $tagName = strtolower($htmlParser->getFirstTagName($v));
3759
                $cfg = $conf['externalBlocks.'][$tagName . '.'];
3760
                if ($cfg['callRecursive']) {
3761
                    $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf);
3762
                    if (!$cfg['callRecursive.']['dontWrapSelf']) {
3763
                        if ($cfg['callRecursive.']['alternativeWrap']) {
3764
                            $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']);
3765
                        } else {
3766
                            if (is_array($cfg['callRecursive.']['tagStdWrap.'])) {
3767
                                $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']);
3768
                            }
3769
                            $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>';
3770
                        }
3771
                    }
3772
                } elseif ($cfg['HTMLtableCells']) {
3773
                    $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]);
3774
                    foreach ($rowParts as $kk => $vv) {
3775
                        if ($kk % 2) {
3776
                            $colParts = $htmlParser->splitIntoBlock('td,th', $vv);
3777
                            $cc = 0;
3778
                            foreach ($colParts as $kkk => $vvv) {
3779
                                if ($kkk % 2) {
3780
                                    $cc++;
3781
                                    $tag = $htmlParser->getFirstTag($vvv);
3782
                                    $tagName = strtolower($htmlParser->getFirstTagName($vvv));
3783
                                    $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv);
3784
                                    if ($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] || !isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && $cfg['HTMLtableCells.']['default.']['callRecursive']) {
3785
                                        if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs']) {
3786
                                            $colParts[$kkk] = str_replace('</p><p>', '</p>' . LF . '<p>', $colParts[$kkk]);
3787
                                        }
3788
                                        $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf);
3789
                                    }
3790
                                    $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'])
3791
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3792
                                        : $cfg['HTMLtableCells.']['default.']['tagStdWrap.'];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3793
                                    if (is_array($tagStdWrap)) {
3794
                                        $tag = $this->stdWrap($tag, $tagStdWrap);
3795
                                    }
3796
                                    $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'])
3797
                                        ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3798
                                        : $cfg['HTMLtableCells.']['default.']['stdWrap.'];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3799
                                    if (is_array($stdWrap)) {
3800
                                        $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap);
3801
                                    }
3802
                                    $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>';
3803
                                }
3804
                            }
3805
                            $rowParts[$kk] = implode('', $colParts);
3806
                        }
3807
                    }
3808
                    $parts[$k] = implode('', $rowParts);
3809
                }
3810
                if (is_array($cfg['stdWrap.'])) {
3811
                    $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
3812
                }
3813
            } else {
3814
                $parts[$k] = $this->_parseFunc($parts[$k], $conf);
3815
            }
3816
        }
3817
        return implode('', $parts);
3818
    }
3819
3820
    /**
3821
     * Helper function for parseFunc()
3822
     *
3823
     * @param string $theValue The value to process.
3824
     * @param array $conf TypoScript configuration for parseFunc
3825
     * @return string The processed value
3826
     * @internal
3827
     * @see parseFunc()
3828
     */
3829
    public function _parseFunc($theValue, $conf)
3830
    {
3831
        if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
3832
            return $theValue;
3833
        }
3834
        // Indicates that the data is from within a tag.
3835
        $inside = false;
3836
        // Pointer to the total string position
3837
        $pointer = 0;
3838
        // Loaded with the current typo-tag if any.
3839
        $currentTag = null;
3840
        $stripNL = 0;
3841
        $contentAccum = [];
3842
        $contentAccumP = 0;
3843
        $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? ''));
3844
        $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? ''));
3845
        $totalLen = strlen($theValue);
3846
        do {
3847
            if (!$inside) {
3848
                if ($currentTag === null) {
3849
                    // These operations should only be performed on code outside the typotags...
3850
                    // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/'
3851
                    $len_p = 0;
3852
                    $c = 100;
3853
                    do {
3854
                        $len = strcspn(substr($theValue, $pointer + $len_p), '<');
3855
                        $len_p += $len + 1;
3856
                        $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1)));
3857
                        $c--;
3858
                    } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47);
3859
                    $len = $len_p - 1;
3860
                } else {
3861
                    $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, $currentTag[0]);
3862
                }
3863
                // $data is the content until the next <tag-start or end is detected.
3864
                // In case of a currentTag set, this would mean all data between the start- and end-tags
3865
                $data = substr($theValue, $pointer, $len);
3866
                if ($data !== false) {
3867
                    if ($stripNL) {
3868
                        // If the previous tag was set to strip NewLines in the beginning of the next data-chunk.
3869
                        $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data);
3870
                    }
3871
                    // These operations should only be performed on code outside the tags...
3872
                    if (!is_array($currentTag)) {
3873
                        // Constants
3874
                        $tsfe = $this->getTypoScriptFrontendController();
3875
                        $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null;
3876
                        if (!empty($conf['constants']) && is_array($tmpConstants)) {
3877
                            foreach ($tmpConstants as $key => $val) {
3878
                                if (is_string($val)) {
3879
                                    $data = str_replace('###' . $key . '###', $val, $data);
3880
                                }
3881
                            }
3882
                        }
3883
                        // Short
3884
                        if (isset($conf['short.']) && is_array($conf['short.'])) {
3885
                            $shortWords = $conf['short.'];
3886
                            krsort($shortWords);
3887
                            foreach ($shortWords as $key => $val) {
3888
                                if (is_string($val)) {
3889
                                    $data = str_replace($key, $val, $data);
3890
                                }
3891
                            }
3892
                        }
3893
                        // stdWrap
3894
                        if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) {
3895
                            $data = $this->stdWrap($data, $conf['plainTextStdWrap.']);
3896
                        }
3897
                        // userFunc
3898
                        if ($conf['userFunc'] ?? false) {
3899
                            $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $data);
3900
                        }
3901
                        // Makelinks: (Before search-words as we need the links to be generated when searchwords go on...!)
3902
                        if ($conf['makelinks'] ?? false) {
3903
                            $data = $this->http_makelinks($data, $conf['makelinks.']['http.']);
3904
                            $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []);
3905
                        }
3906
                        // Search Words:
3907
                        if ($tsfe->no_cache && $conf['sword'] && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) {
3908
                            $newstring = '';
3909
                            do {
3910
                                $pregSplitMode = 'i';
3911
                                if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) {
3912
                                    $pregSplitMode = '';
3913
                                }
3914
                                $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2);
3915
                                $newstring .= $pieces[0];
3916
                                $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1]));
3917
                                $inTag = false;
3918
                                if (strpos($pieces[0], '<') !== false || strpos($pieces[0], '>') !== false) {
3919
                                    // Returns TRUE, if a '<' is closer to the string-end than '>'.
3920
                                    // This is the case if we're INSIDE a tag (that could have been
3921
                                    // made by makelinks...) and we must secure, that the inside of a tag is
3922
                                    // not marked up.
3923
                                    $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>');
3924
                                }
3925
                                // The searchword:
3926
                                $match = substr($data, strlen($pieces[0]), $match_len);
3927
                                if (trim($match) && strlen($match) > 1 && !$inTag) {
3928
                                    $match = $this->wrap($match, $conf['sword']);
3929
                                }
3930
                                // Concatenate the Search Word again.
3931
                                $newstring .= $match;
3932
                                $data = $pieces[1];
3933
                            } while ($pieces[1]);
3934
                            $data = $newstring;
3935
                        }
3936
                    }
3937
                    // Search for tags to process in current data and
3938
                    // call this method recursively if found
3939
                    if (strpos($data, '<') !== false) {
3940
                        foreach ($conf['tags.'] as $tag => $tagConfig) {
3941
                            if (strpos($data, '<' . $tag) !== false) {
3942
                                $data = $this->_parseFunc($data, $conf);
3943
                                break;
3944
                            }
3945
                        }
3946
                    }
3947
                    $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3948
                        ? $contentAccum[$contentAccumP] . $data
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3949
                        : $data;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3950
                }
3951
                $inside = true;
3952
            } else {
3953
                // tags
3954
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
3955
                $data = substr($theValue, $pointer, $len);
3956
                if (StringUtility::endsWith($data, '/>') && strpos($data, '<link ') !== 0) {
3957
                    $tagContent = substr($data, 1, -2);
3958
                } else {
3959
                    $tagContent = substr($data, 1, -1);
3960
                }
3961
                $tag = explode(' ', trim($tagContent), 2);
3962
                $tag[0] = strtolower($tag[0]);
3963
                // end tag like </li>
3964
                if ($tag[0][0] === '/') {
3965
                    $tag[0] = substr($tag[0], 1);
3966
                    $tag['out'] = 1;
3967
                }
3968
                if ($conf['tags.'][$tag[0]] ?? false) {
3969
                    $treated = false;
3970
                    $stripNL = false;
3971
                    // in-tag
3972
                    if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) {
3973
                        // $currentTag (array!) is the tag we are currently processing
3974
                        $currentTag = $tag;
3975
                        $contentAccumP++;
3976
                        $treated = true;
3977
                        // in-out-tag: img and other empty tags
3978
                        if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', $tag[0])) {
3979
                            $tag['out'] = 1;
3980
                        }
3981
                    }
3982
                    // out-tag
3983
                    if ($currentTag[0] === $tag[0] && isset($tag['out']) && $tag['out']) {
3984
                        $theName = $conf['tags.'][$tag[0]];
3985
                        $theConf = $conf['tags.'][$tag[0] . '.'];
3986
                        // This flag indicates, that NL- (13-10-chars) should be stripped first and last.
3987
                        $stripNL = (bool)($theConf['stripNL'] ?? false);
3988
                        // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content.
3989
                        $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false);
3990
                        $this->parameters = [];
3991
                        if (isset($currentTag[1])) {
3992
                            $params = GeneralUtility::get_tag_attributes($currentTag[1]);
3993
                            if (is_array($params)) {
3994
                                foreach ($params as $option => $val) {
3995
                                    $this->parameters[strtolower($option)] = $val;
3996
                                }
3997
                            }
3998
                            $this->parameters['allParams'] = trim($currentTag[1]);
3999
                        }
4000
                        // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer.
4001
                        // $stripNL depends on the configuration of the current tag
4002
                        if ($stripNL) {
4003
                            $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1]);
4004
                            $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP]);
4005
                            $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP]);
4006
                        }
4007
                        $this->data[$this->currentValKey] = $contentAccum[$contentAccumP];
4008
                        $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]);
4009
                        // fetch the content object
4010
                        $contentAccum[$contentAccumP] = $newInput;
4011
                        $contentAccumP++;
4012
                        // If the TypoTag section
4013
                        if (!$breakOut) {
4014
                            if (!isset($contentAccum[$contentAccumP - 2])) {
4015
                                $contentAccum[$contentAccumP - 2] = '';
4016
                            }
4017
                            $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? '');
4018
                            unset($contentAccum[$contentAccumP]);
4019
                            unset($contentAccum[$contentAccumP - 1]);
4020
                            $contentAccumP -= 2;
4021
                        }
4022
                        $currentTag = null;
4023
                        $treated = true;
4024
                    }
4025
                    // other tags
4026
                    if (!$treated) {
4027
                        $contentAccum[$contentAccumP] .= $data;
4028
                    }
4029
                } else {
4030
                    // If a tag was not a typo tag, then it is just added to the content
4031
                    $stripNL = false;
4032
                    if (GeneralUtility::inList($allowTags, $tag[0]) ||
4033
                        ($denyTags !== '*' && !GeneralUtility::inList($denyTags, $tag[0]))) {
4034
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
4035
                            ? $contentAccum[$contentAccumP] . $data
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
4036
                            : $data;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
4037
                    } else {
4038
                        $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
4039
                            ? $contentAccum[$contentAccumP] . htmlspecialchars($data)
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
4040
                            : htmlspecialchars($data);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
4041
                    }
4042
                }
4043
                $inside = false;
4044
            }
4045
            $pointer += $len;
4046
        } while ($pointer < $totalLen);
4047
        // Parsing nonTypoTag content (all even keys):
4048
        reset($contentAccum);
4049
        $contentAccumCount = count($contentAccum);
4050
        for ($a = 0; $a < $contentAccumCount; $a++) {
4051
            if ($a % 2 != 1) {
4052
                // stdWrap
4053
                if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) {
4054
                    $contentAccum[$a] = $this->stdWrap($contentAccum[$a], $conf['nonTypoTagStdWrap.']);
4055
                }
4056
                // userFunc
4057
                if (!empty($conf['nonTypoTagUserFunc'])) {
4058
                    $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'], $contentAccum[$a]);
4059
                }
4060
            }
4061
        }
4062
        return implode('', $contentAccum);
4063
    }
4064
4065
    /**
4066
     * Lets you split the content by LF and process each line independently. Used to format content made with the RTE.
4067
     *
4068
     * @param string $theValue The input value
4069
     * @param array $conf TypoScript options
4070
     * @return string The processed input value being returned; Splitted lines imploded by LF again.
4071
     * @internal
4072
     */
4073
    public function encaps_lineSplit($theValue, $conf)
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::encaps_lineSplit" is not in camel caps format
Loading history...
4074
    {
4075
        if ((string)$theValue === '') {
4076
            return '';
4077
        }
4078
        $lParts = explode(LF, $theValue);
4079
4080
        // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line.
4081
        $lastPartIndex = count($lParts) - 1;
4082
        if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') {
4083
            array_pop($lParts);
4084
        }
4085
4086
        $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList']), true);
4087
        $nonWrappedTag = $conf['nonWrappedTag'];
4088
        $defaultAlign = isset($conf['defaultAlign.'])
4089
            ? trim($this->stdWrap($conf['defaultAlign'] ?? '', $conf['defaultAlign.']))
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
4090
            : trim($conf['defaultAlign'] ?? '');
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
4091
4092
        $str_content = '';
4093
        foreach ($lParts as $k => $l) {
4094
            $sameBeginEnd = 0;
4095
            $emptyTag = false;
4096
            $l = trim($l);
4097
            $attrib = [];
4098
            $nonWrapped = false;
4099
            $tagName = '';
4100
            if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') {
4101
                $fwParts = explode('>', substr($l, 1), 2);
4102
                [$tagName] = explode(' ', $fwParts[0], 2);
4103
                if (!$fwParts[1]) {
4104
                    if (substr($tagName, -1) === '/') {
4105
                        $tagName = substr($tagName, 0, -1);
4106
                    }
4107
                    if (substr($fwParts[0], -1) === '/') {
4108
                        $sameBeginEnd = 1;
4109
                        $emptyTag = true;
4110
                        $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>');
4111
                    }
4112
                } else {
4113
                    $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2);
4114
                    $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>');
4115
                    $str_content = $backParts[0];
4116
                    $sameBeginEnd = substr(strtolower($backParts[1]), 1, strlen($tagName)) === strtolower($tagName);
4117
                }
4118
            }
4119
            if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) {
4120
                $uTagName = strtoupper($tagName);
4121
                $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName);
4122
            } else {
4123
                $uTagName = strtoupper($nonWrappedTag);
4124
                // The line will be wrapped: $uTagName should not be an empty tag
4125
                $emptyTag = false;
4126
                $str_content = $lParts[$k];
4127
                $nonWrapped = true;
4128
                $attrib = [];
4129
            }
4130
            // Wrapping all inner-content:
4131
            if (is_array($conf['innerStdWrap_all.'])) {
4132
                $str_content = $this->stdWrap($str_content, $conf['innerStdWrap_all.']);
4133
            }
4134
            if ($uTagName) {
4135
                // Setting common attributes
4136
                if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) {
4137
                    foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) {
4138
                        if (!is_array($vv)) {
4139
                            if ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'blank') {
4140
                                if ((string)($attrib[$kk] ?? '') === '') {
4141
                                    $attrib[$kk] = $vv;
4142
                                }
4143
                            } elseif ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'exists') {
4144
                                if (!isset($attrib[$kk])) {
4145
                                    $attrib[$kk] = $vv;
4146
                                }
4147
                            } else {
4148
                                $attrib[$kk] = $vv;
4149
                            }
4150
                        }
4151
                    }
4152
                }
4153
                // Wrapping all inner-content:
4154
                if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) {
4155
                    $str_content = $this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']);
4156
                }
4157
                // Default align
4158
                if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) {
4159
                    $attrib['align'] = $defaultAlign;
4160
                }
4161
                $params = GeneralUtility::implodeAttributes($attrib, true);
4162
                if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) {
4163
                    $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
4164
                    if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) {
4165
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />';
4166
                    } else {
4167
                        $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>';
4168
                    }
4169
                }
4170
            }
4171
            if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) {
4172
                $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']);
4173
            }
4174
            $lParts[$k] = $str_content;
4175
        }
4176
        return implode(LF, $lParts);
4177
    }
4178
4179
    /**
4180
     * Finds URLS in text and makes it to a real link.
4181
     * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link,
4182
     * linking to the URL we should have found.
4183
     *
4184
     * @param string $data The string in which to search for "http://
4185
     * @param array $conf Configuration for makeLinks, see link
4186
     * @return string The processed input string, being returned.
4187
     * @see _parseFunc()
4188
     */
4189
    public function http_makelinks($data, $conf)
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::http_makelinks" is not in camel caps format
Loading history...
4190
    {
4191
        $parts = [];
4192
        $aTagParams = $this->getATagParams($conf);
4193
        $textstr = '';
4194
        foreach (['http://', 'https://'] as $scheme) {
4195
            $textpieces = explode($scheme, $data);
4196
            $pieces = count($textpieces);
4197
            $textstr = $textpieces[0];
4198
            for ($i = 1; $i < $pieces; $i++) {
4199
                $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
4200
                if (trim(substr($textstr, -1)) === '' && $len) {
4201
                    $lastChar = substr($textpieces[$i], $len - 1, 1);
4202
                    if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) {
4203
                        $len--;
4204
                    }
4205
                    // Included '\/' 3/12
4206
                    $parts[0] = substr($textpieces[$i], 0, $len);
4207
                    $parts[1] = substr($textpieces[$i], $len);
4208
                    $keep = $conf['keep'];
4209
                    $linkParts = parse_url($scheme . $parts[0]);
4210
                    $linktxt = '';
4211
                    if (strpos($keep, 'scheme') !== false) {
4212
                        $linktxt = $scheme;
4213
                    }
4214
                    $linktxt .= $linkParts['host'];
4215
                    if (strpos($keep, 'path') !== false) {
4216
                        $linktxt .= $linkParts['path'];
4217
                        // Added $linkParts['query'] 3/12
4218
                        if (strpos($keep, 'query') !== false && $linkParts['query']) {
4219
                            $linktxt .= '?' . $linkParts['query'];
4220
                        } elseif ($linkParts['path'] === '/') {
4221
                            $linktxt = substr($linktxt, 0, -1);
4222
                        }
4223
                    }
4224
                    if (isset($conf['extTarget'])) {
4225
                        if (isset($conf['extTarget.'])) {
4226
                            $target = $this->stdWrap($conf['extTarget'], $conf['extTarget.']);
4227
                        } else {
4228
                            $target = $conf['extTarget'];
4229
                        }
4230
                    } else {
4231
                        $target = $this->getTypoScriptFrontendController()->extTarget;
4232
                    }
4233
4234
                    // check for jump URLs or similar
4235
                    $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf);
4236
4237
                    $res = '<a href="' . htmlspecialchars($linkUrl) . '"'
4238
                        . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '')
4239
                        . $aTagParams . $this->extLinkATagParams('http://' . $parts[0], 'url') . '>';
4240
4241
                    $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
4242
                    if ((string)$conf['ATagBeforeWrap'] !== '') {
4243
                        $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
4244
                    } else {
4245
                        $res = $this->wrap($res . $linktxt . '</a>', $wrap);
4246
                    }
4247
                    $textstr .= $res . $parts[1];
4248
                } else {
4249
                    $textstr .= $scheme . $textpieces[$i];
4250
                }
4251
            }
4252
            $data = $textstr;
4253
        }
4254
        return $textstr;
4255
    }
4256
4257
    /**
4258
     * Will find all strings prefixed with "mailto:" in the $data string and make them into a link,
4259
     * linking to the email address they point to.
4260
     *
4261
     * @param string $data The string in which to search for "mailto:
4262
     * @param array $conf Configuration for makeLinks, see link
4263
     * @return string The processed input string, being returned.
4264
     * @see _parseFunc()
4265
     */
4266
    public function mailto_makelinks($data, $conf)
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::mailto_makelinks" is not in camel caps format
Loading history...
4267
    {
4268
        $parts = [];
4269
        // http-split
4270
        $aTagParams = $this->getATagParams($conf);
4271
        $textpieces = explode('mailto:', $data);
4272
        $pieces = count($textpieces);
4273
        $textstr = $textpieces[0];
4274
        $tsfe = $this->getTypoScriptFrontendController();
4275
        for ($i = 1; $i < $pieces; $i++) {
4276
            $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
4277
            if (trim(substr($textstr, -1)) === '' && $len) {
4278
                $lastChar = substr($textpieces[$i], $len - 1, 1);
4279
                if (!preg_match('/[A-Za-z0-9]/', $lastChar)) {
4280
                    $len--;
4281
                }
4282
                $parts[0] = substr($textpieces[$i], 0, $len);
4283
                $parts[1] = substr($textpieces[$i], $len);
4284
                $linktxt = preg_replace('/\\?.*/', '', $parts[0]);
4285
                [$mailToUrl, $linktxt] = $this->getMailTo($parts[0], $linktxt);
4286
                $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl);
4287
                $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>';
4288
                $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
4289
                if ((string)$conf['ATagBeforeWrap'] !== '') {
4290
                    $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
4291
                } else {
4292
                    $res = $this->wrap($res . $linktxt . '</a>', $wrap);
4293
                }
4294
                $textstr .= $res . $parts[1];
4295
            } else {
4296
                $textstr .= 'mailto:' . $textpieces[$i];
4297
            }
4298
        }
4299
        return $textstr;
4300
    }
4301
4302
    /**
4303
     * Creates and returns a TypoScript "imgResource".
4304
     * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER".
4305
     * In the first case a current image is returned, possibly scaled down or otherwise processed.
4306
     * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines.
4307
     * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']);
4308
     *
4309
     * Structure of the returned info array:
4310
     *  0 => width
4311
     *  1 => height
4312
     *  2 => file extension
4313
     *  3 => file name
4314
     *  origFile => original file name
4315
     *  origFile_mtime => original file mtime
4316
     *  -- only available if processed via FAL: --
4317
     *  originalFile => original file object
4318
     *  processedFile => processed file object
4319
     *  fileCacheHash => checksum of processed file
4320
     *
4321
     * @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.
4322
     * @param array $fileArray TypoScript properties for the imgResource type
4323
     * @return array|null Returns info-array
4324
     * @see cImage()
4325
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder
4326
     */
4327
    public function getImgResource($file, $fileArray)
4328
    {
4329
        $importedFile = null;
4330
        if (empty($file) && empty($fileArray)) {
4331
            return null;
4332
        }
4333
        if (!is_array($fileArray)) {
0 ignored issues
show
introduced by
The condition is_array($fileArray) is always true.
Loading history...
4334
            $fileArray = (array)$fileArray;
4335
        }
4336
        $imageResource = null;
4337
        if ($file === 'GIFBUILDER') {
4338
            $gifCreator = GeneralUtility::makeInstance(GifBuilder::class);
4339
            $theImage = '';
4340
            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) {
4341
                $gifCreator->start($fileArray, $this->data);
4342
                $theImage = $gifCreator->gifBuild();
4343
            }
4344
            $imageResource = $gifCreator->getImageDimensions($theImage);
4345
            $imageResource['origFile'] = $theImage;
4346
        } else {
4347
            if ($file instanceof File) {
4348
                $fileObject = $file;
4349
            } elseif ($file instanceof FileReference) {
4350
                $fileObject = $file->getOriginalFile();
4351
            } else {
4352
                try {
4353
                    if (isset($fileArray['import.']) && $fileArray['import.']) {
4354
                        $importedFile = trim($this->stdWrap('', $fileArray['import.']));
4355
                        if (!empty($importedFile)) {
4356
                            $file = $importedFile;
4357
                        }
4358
                    }
4359
4360
                    if (MathUtility::canBeInterpretedAsInteger($file)) {
4361
                        $treatIdAsReference = isset($fileArray['treatIdAsReference.']) ? $this->stdWrap($fileArray['treatIdAsReference'], $fileArray['treatIdAsReference.']) : $fileArray['treatIdAsReference'];
4362
                        if (!empty($treatIdAsReference)) {
4363
                            $file = $this->getResourceFactory()->getFileReferenceObject($file);
4364
                            $fileObject = $file->getOriginalFile();
4365
                        } else {
4366
                            $fileObject = $this->getResourceFactory()->getFileObject($file);
4367
                        }
4368
                    } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier
4369
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4370
                    } else {
4371
                        if (isset($importedFile) && !empty($importedFile) && !empty($fileArray['import'])) {
4372
                            $file = $fileArray['import'] . $file;
4373
                        }
4374
                        $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4375
                    }
4376
                } catch (Exception $exception) {
4377
                    $this->logger->warning('The image "' . $file . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]);
4378
                    return null;
4379
                }
4380
            }
4381
            if ($fileObject instanceof File) {
4382
                $processingConfiguration = [];
4383
                $processingConfiguration['width'] = isset($fileArray['width.']) ? $this->stdWrap($fileArray['width'], $fileArray['width.']) : $fileArray['width'];
4384
                $processingConfiguration['height'] = isset($fileArray['height.']) ? $this->stdWrap($fileArray['height'], $fileArray['height.']) : $fileArray['height'];
4385
                $processingConfiguration['fileExtension'] = isset($fileArray['ext.']) ? $this->stdWrap($fileArray['ext'], $fileArray['ext.']) : $fileArray['ext'];
4386
                $processingConfiguration['maxWidth'] = isset($fileArray['maxW.']) ? (int)$this->stdWrap($fileArray['maxW'], $fileArray['maxW.']) : (int)$fileArray['maxW'];
4387
                $processingConfiguration['maxHeight'] = isset($fileArray['maxH.']) ? (int)$this->stdWrap($fileArray['maxH'], $fileArray['maxH.']) : (int)$fileArray['maxH'];
4388
                $processingConfiguration['minWidth'] = isset($fileArray['minW.']) ? (int)$this->stdWrap($fileArray['minW'], $fileArray['minW.']) : (int)$fileArray['minW'];
4389
                $processingConfiguration['minHeight'] = isset($fileArray['minH.']) ? (int)$this->stdWrap($fileArray['minH'], $fileArray['minH.']) : (int)$fileArray['minH'];
4390
                $processingConfiguration['noScale'] = isset($fileArray['noScale.']) ? $this->stdWrap($fileArray['noScale'], $fileArray['noScale.']) : $fileArray['noScale'];
4391
                $processingConfiguration['additionalParameters'] = isset($fileArray['params.']) ? $this->stdWrap($fileArray['params'], $fileArray['params.']) : $fileArray['params'];
4392
                $processingConfiguration['frame'] = isset($fileArray['frame.']) ? (int)$this->stdWrap($fileArray['frame'], $fileArray['frame.']) : (int)$fileArray['frame'];
4393
                if ($file instanceof FileReference) {
4394
                    $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($file, $fileArray);
4395
                } else {
4396
                    $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray);
4397
                }
4398
4399
                // Possibility to cancel/force profile extraction
4400
                // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand']
4401
                if (isset($fileArray['stripProfile'])) {
4402
                    $processingConfiguration['stripProfile'] = $fileArray['stripProfile'];
4403
                }
4404
                // Check if we can handle this type of file for editing
4405
                if ($fileObject->isImage()) {
4406
                    $maskArray = $fileArray['m.'];
4407
                    // Must render mask images and include in hash-calculating
4408
                    // - otherwise we cannot be sure the filename is unique for the setup!
4409
                    if (is_array($maskArray)) {
4410
                        $mask = $this->getImgResource($maskArray['mask'], $maskArray['mask.']);
4411
                        $bgImg = $this->getImgResource($maskArray['bgImg'], $maskArray['bgImg.']);
4412
                        $bottomImg = $this->getImgResource($maskArray['bottomImg'], $maskArray['bottomImg.']);
4413
                        $bottomImg_mask = $this->getImgResource($maskArray['bottomImg_mask'], $maskArray['bottomImg_mask.']);
4414
4415
                        $processingConfiguration['maskImages']['maskImage'] = $mask['processedFile'];
4416
                        $processingConfiguration['maskImages']['backgroundImage'] = $bgImg['processedFile'];
4417
                        $processingConfiguration['maskImages']['maskBottomImage'] = $bottomImg['processedFile'];
4418
                        $processingConfiguration['maskImages']['maskBottomImageMask'] = $bottomImg_mask['processedFile'];
4419
                    }
4420
                    $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
4421
                    if ($processedFileObject->isProcessed()) {
4422
                        $imageResource = [
4423
                            0 => (int)$processedFileObject->getProperty('width'),
4424
                            1 => (int)$processedFileObject->getProperty('height'),
4425
                            2 => $processedFileObject->getExtension(),
4426
                            3 => $processedFileObject->getPublicUrl(),
4427
                            'origFile' => $fileObject->getPublicUrl(),
4428
                            'origFile_mtime' => $fileObject->getModificationTime(),
4429
                            // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
4430
                            // in order for the setup-array to create a unique filename hash.
4431
                            'originalFile' => $fileObject,
4432
                            'processedFile' => $processedFileObject
4433
                        ];
4434
                    }
4435
                }
4436
            }
4437
        }
4438
        // If image was processed by GIFBUILDER:
4439
        // ($imageResource indicates that it was processed the regular way)
4440
        if (!isset($imageResource)) {
4441
            try {
4442
                $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize((string)$file);
4443
                $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB');
4444
                $info['origFile'] = $theImage;
4445
                // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash.
4446
                $info['origFile_mtime'] = @filemtime($theImage);
4447
                $imageResource = $info;
4448
            } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
4449
                // do nothing in case the file path is invalid
4450
            }
4451
        }
4452
        // Hook 'getImgResource': Post-processing of image resources
4453
        if (isset($imageResource)) {
4454
            /** @var ContentObjectGetImageResourceHookInterface $hookObject */
4455
            foreach ($this->getGetImgResourceHookObjects() as $hookObject) {
4456
                $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this);
4457
            }
4458
        }
4459
        return $imageResource;
4460
    }
4461
4462
    /**
4463
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4464
     * or null if the crop settings or crop area is empty.
4465
     *
4466
     * The cropArea from file reference is used, if not set in TypoScript.
4467
     *
4468
     * Example TypoScript settings:
4469
     * file.crop =
4470
     * OR
4471
     * file.crop = 50,50,100,100
4472
     * OR
4473
     * file.crop.data = file:current:crop
4474
     *
4475
     * @param FileReference $fileReference
4476
     * @param array $fileArray TypoScript properties for the imgResource type
4477
     * @return Area|null
4478
     */
4479
    protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray)
4480
    {
4481
        // Use cropping area from file reference if nothing is configured in TypoScript.
4482
        if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) {
4483
            // Set crop variant from TypoScript settings. If not set, use default.
4484
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4485
            $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant);
4486
            return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference);
4487
        }
4488
4489
        return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray);
4490
    }
4491
4492
    /**
4493
     * Returns an ImageManipulation\Area object for the given cropVariant (or 'default')
4494
     * or null if the crop settings or crop area is empty.
4495
     *
4496
     * @param FileInterface $file
4497
     * @param array $fileArray
4498
     * @return Area|null
4499
     */
4500
    protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray)
4501
    {
4502
        /** @var Area $cropArea */
4503
        $cropArea = null;
4504
        // Resolve TypoScript configured cropping.
4505
        $cropSettings = isset($fileArray['crop.'])
4506
            ? $this->stdWrap($fileArray['crop'], $fileArray['crop.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
4507
            : ($fileArray['crop'] ?? null);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
4508
4509
        if (is_string($cropSettings)) {
4510
            // Set crop variant from TypoScript settings. If not set, use default.
4511
            $cropVariant = $fileArray['cropVariant'] ?? 'default';
4512
            // Get cropArea from CropVariantCollection, if cropSettings is a valid json.
4513
            // CropVariantCollection::create does json_decode.
4514
            $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant);
4515
            $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file);
4516
4517
            // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100
4518
            if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) {
4519
                $cropSettings = explode(',', $cropSettings);
4520
                if (count($cropSettings) === 4) {
4521
                    $stringCropArea = GeneralUtility::makeInstance(
4522
                        Area::class,
4523
                        ...$cropSettings
4524
                    );
4525
                    $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea;
4526
                }
4527
            }
4528
        }
4529
4530
        return $cropArea;
4531
    }
4532
4533
    /**
4534
     * Takes a JSON string and creates CropVariantCollection and fetches the corresponding
4535
     * CropArea for that.
4536
     *
4537
     * @param string $cropSettings
4538
     * @param string $cropVariant
4539
     * @return Area
4540
     */
4541
    protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area
4542
    {
4543
        return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant);
4544
    }
4545
4546
    /***********************************************
4547
     *
4548
     * Data retrieval etc.
4549
     *
4550
     ***********************************************/
4551
    /**
4552
     * 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.
4553
     *
4554
     * @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)
4555
     * @return string|null
4556
     */
4557
    public function getFieldVal($field)
4558
    {
4559
        if (strpos($field, '//') === false) {
4560
            return $this->data[trim($field)] ?? null;
4561
        }
4562
        $sections = GeneralUtility::trimExplode('//', $field, true);
4563
        foreach ($sections as $k) {
4564
            if ((string)$this->data[$k] !== '') {
4565
                return $this->data[$k];
4566
            }
4567
        }
4568
4569
        return '';
4570
    }
4571
4572
    /**
4573
     * 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.
4574
     *
4575
     * @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)
4576
     * @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.
4577
     * @return string The value fetched
4578
     * @see getFieldVal()
4579
     */
4580
    public function getData($string, $fieldArray = null)
4581
    {
4582
        $tsfe = $this->getTypoScriptFrontendController();
4583
        if (!is_array($fieldArray)) {
4584
            $fieldArray = $tsfe->page;
4585
        }
4586
        $retVal = '';
4587
        $sections = explode('//', $string);
4588
        foreach ($sections as $secKey => $secVal) {
4589
            if ($retVal) {
4590
                break;
4591
            }
4592
            $parts = explode(':', $secVal, 2);
4593
            $type = strtolower(trim($parts[0]));
4594
            $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout'];
4595
            $key = trim($parts[1] ?? '');
4596
            if (($key != '') || in_array($type, $typesWithOutParameters)) {
4597
                switch ($type) {
4598
                    case 'gp':
4599
                        // Merge GET and POST and get $key out of the merged array
4600
                        $getPostArray = GeneralUtility::_GET();
4601
                        ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST());
4602
                        $retVal = $this->getGlobal($key, $getPostArray);
4603
                        break;
4604
                    case 'tsfe':
4605
                        $retVal = $this->getGlobal('TSFE|' . $key);
4606
                        break;
4607
                    case 'getenv':
4608
                        $retVal = getenv($key);
4609
                        break;
4610
                    case 'getindpenv':
4611
                        $retVal = $this->getEnvironmentVariable($key);
4612
                        break;
4613
                    case 'field':
4614
                        $retVal = $this->getGlobal($key, $fieldArray);
4615
                        break;
4616
                    case 'file':
4617
                        $retVal = $this->getFileDataKey($key);
4618
                        break;
4619
                    case 'parameters':
4620
                        $retVal = $this->parameters[$key];
4621
                        break;
4622
                    case 'register':
4623
                        $retVal = $tsfe->register[$key] ?? null;
4624
                        break;
4625
                    case 'global':
4626
                        $retVal = $this->getGlobal($key);
4627
                        break;
4628
                    case 'level':
4629
                        $retVal = count($tsfe->tmpl->rootLine) - 1;
4630
                        break;
4631
                    case 'leveltitle':
4632
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4633
                        $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine);
4634
                        $retVal = $this->rootLineValue($numericKey, 'title', strtolower($keyParts[1] ?? '') === 'slide');
4635
                        break;
4636
                    case 'levelmedia':
4637
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4638
                        $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine);
4639
                        $retVal = $this->rootLineValue($numericKey, 'media', strtolower($keyParts[1] ?? '') === 'slide');
4640
                        break;
4641
                    case 'leveluid':
4642
                        $numericKey = $this->getKey($key, $tsfe->tmpl->rootLine);
0 ignored issues
show
Bug introduced by
$key of type string is incompatible with the type integer expected by parameter $key of TYPO3\CMS\Frontend\Conte...bjectRenderer::getKey(). ( Ignorable by Annotation )

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

4642
                        $numericKey = $this->getKey(/** @scrutinizer ignore-type */ $key, $tsfe->tmpl->rootLine);
Loading history...
4643
                        $retVal = $this->rootLineValue($numericKey, 'uid');
4644
                        break;
4645
                    case 'levelfield':
4646
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4647
                        $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine);
4648
                        $retVal = $this->rootLineValue($numericKey, $keyParts[1], strtolower($keyParts[2] ?? '') === 'slide');
4649
                        break;
4650
                    case 'fullrootline':
4651
                        $keyParts = GeneralUtility::trimExplode(',', $key);
4652
                        $fullKey = (int)$keyParts[0] - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine);
4653
                        if ($fullKey >= 0) {
4654
                            $retVal = $this->rootLineValue($fullKey, $keyParts[1], stristr($keyParts[2] ?? '', 'slide'), $tsfe->rootLine);
0 ignored issues
show
Bug introduced by
stristr($keyParts[2] ?? '', 'slide') of type string is incompatible with the type boolean expected by parameter $slideBack of TYPO3\CMS\Frontend\Conte...nderer::rootLineValue(). ( Ignorable by Annotation )

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

4654
                            $retVal = $this->rootLineValue($fullKey, $keyParts[1], /** @scrutinizer ignore-type */ stristr($keyParts[2] ?? '', 'slide'), $tsfe->rootLine);
Loading history...
4655
                        }
4656
                        break;
4657
                    case 'date':
4658
                        if (!$key) {
4659
                            $key = 'd/m Y';
4660
                        }
4661
                        $retVal = date($key, $GLOBALS['EXEC_TIME']);
4662
                        break;
4663
                    case 'page':
4664
                        $retVal = $tsfe->page[$key];
4665
                        break;
4666
                    case 'pagelayout':
4667
                        $retVal = GeneralUtility::makeInstance(PageLayoutResolver::class)
4668
                            ->getLayoutForPage($tsfe->page, $tsfe->rootLine);
4669
                        break;
4670
                    case 'current':
4671
                        $retVal = $this->data[$this->currentValKey] ?? null;
4672
                        break;
4673
                    case 'db':
4674
                        $selectParts = GeneralUtility::trimExplode(':', $key);
4675
                        $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]);
4676
                        if (is_array($db_rec) && $selectParts[2]) {
4677
                            $retVal = $db_rec[$selectParts[2]];
4678
                        }
4679
                        break;
4680
                    case 'lll':
4681
                        $retVal = $tsfe->sL('LLL:' . $key);
4682
                        break;
4683
                    case 'path':
4684
                        try {
4685
                            $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key);
4686
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
4687
                            // do nothing in case the file path is invalid
4688
                            $retVal = null;
4689
                        }
4690
                        break;
4691
                    case 'cobj':
4692
                        switch ($key) {
4693
                            case 'parentRecordNumber':
4694
                                $retVal = $this->parentRecordNumber;
4695
                                break;
4696
                        }
4697
                        break;
4698
                    case 'debug':
4699
                        switch ($key) {
4700
                            case 'rootLine':
4701
                                $retVal = DebugUtility::viewArray($tsfe->tmpl->rootLine);
4702
                                break;
4703
                            case 'fullRootLine':
4704
                                $retVal = DebugUtility::viewArray($tsfe->rootLine);
4705
                                break;
4706
                            case 'data':
4707
                                $retVal = DebugUtility::viewArray($this->data);
4708
                                break;
4709
                            case 'register':
4710
                                $retVal = DebugUtility::viewArray($tsfe->register);
4711
                                break;
4712
                            case 'page':
4713
                                $retVal = DebugUtility::viewArray($tsfe->page);
4714
                                break;
4715
                        }
4716
                        break;
4717
                    case 'flexform':
4718
                        $keyParts = GeneralUtility::trimExplode(':', $key, true);
4719
                        if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) {
4720
                            $flexFormContent = $this->data[$keyParts[0]];
4721
                            if (!empty($flexFormContent)) {
4722
                                $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
4723
                                $flexFormKey = str_replace('.', '|', $keyParts[1]);
4724
                                $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent);
4725
                                $retVal = $this->getGlobal($flexFormKey, $settings);
4726
                            }
4727
                        }
4728
                        break;
4729
                    case 'session':
4730
                        $keyParts = GeneralUtility::trimExplode('|', $key, true);
4731
                        $sessionKey = array_shift($keyParts);
4732
                        $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey);
4733
                        foreach ($keyParts as $keyPart) {
4734
                            if (is_object($retVal)) {
4735
                                $retVal = $retVal->{$keyPart};
4736
                            } elseif (is_array($retVal)) {
4737
                                $retVal = $retVal[$keyPart];
4738
                            } else {
4739
                                $retVal = '';
4740
                                break;
4741
                            }
4742
                        }
4743
                        if (!is_scalar($retVal)) {
4744
                            $retVal = '';
4745
                        }
4746
                        break;
4747
                    case 'context':
4748
                        $context = GeneralUtility::makeInstance(Context::class);
4749
                        [$aspectName, $propertyName] = GeneralUtility::trimExplode(':', $key, true, 2);
4750
                        $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, '');
4751
                        if (is_array($retVal)) {
4752
                            $retVal = implode(',', $retVal);
4753
                        }
4754
                        if (!is_scalar($retVal)) {
4755
                            $retVal = '';
4756
                        }
4757
                        break;
4758
                    case 'site':
4759
                        $site = $this->getTypoScriptFrontendController()->getSite();
4760
                        if ($key === 'identifier') {
4761
                            $retVal = $site->getIdentifier();
4762
                        } elseif ($key === 'base') {
4763
                            $retVal = $site->getBase();
4764
                        } else {
4765
                            try {
4766
                                $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.');
4767
                            } catch (MissingArrayPathException $exception) {
4768
                                $this->logger->warning(sprintf('getData() with "%s" failed', $key), ['exception' => $exception]);
4769
                            }
4770
                        }
4771
                        break;
4772
                    case 'sitelanguage':
4773
                        $siteLanguage = $this->getTypoScriptFrontendController()->getLanguage();
4774
                        $config = $siteLanguage->toArray();
4775
                        if (isset($config[$key])) {
4776
                            $retVal = $config[$key];
4777
                        }
4778
                        break;
4779
                }
4780
            }
4781
4782
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) {
4783
                $hookObject = GeneralUtility::makeInstance($className);
4784
                if (!$hookObject instanceof ContentObjectGetDataHookInterface) {
4785
                    throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480);
4786
                }
4787
                $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4788
                $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $ref);
4789
            }
4790
        }
4791
        return $retVal;
4792
    }
4793
4794
    /**
4795
     * Gets file information. This is a helper function for the getData() method above, which resolves e.g.
4796
     * page.10.data = file:current:title
4797
     * or
4798
     * page.10.data = file:17:title
4799
     *
4800
     * @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.)
4801
     * @return string|int The value as retrieved from the file object.
4802
     */
4803
    protected function getFileDataKey($key)
4804
    {
4805
        [$fileUidOrCurrentKeyword, $requestedFileInformationKey] = explode(':', $key, 3);
4806
        try {
4807
            if ($fileUidOrCurrentKeyword === 'current') {
4808
                $fileObject = $this->getCurrentFile();
4809
            } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) {
4810
                /** @var ResourceFactory $fileFactory */
4811
                $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
4812
                $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword);
0 ignored issues
show
Bug introduced by
$fileUidOrCurrentKeyword of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Core\Resource\...actory::getFileObject(). ( Ignorable by Annotation )

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

4812
                $fileObject = $fileFactory->getFileObject(/** @scrutinizer ignore-type */ $fileUidOrCurrentKeyword);
Loading history...
4813
            } else {
4814
                $fileObject = null;
4815
            }
4816
        } catch (Exception $exception) {
4817
            $this->logger->warning('The file "' . $fileUidOrCurrentKeyword . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]);
4818
            $fileObject = null;
4819
        }
4820
4821
        if ($fileObject instanceof FileInterface) {
4822
            // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here:
4823
            switch ($requestedFileInformationKey) {
4824
                case 'name':
4825
                    return $fileObject->getName();
4826
                case 'uid':
4827
                    if (method_exists($fileObject, 'getUid')) {
4828
                        return $fileObject->getUid();
4829
                    }
4830
                    return 0;
4831
                case 'originalUid':
4832
                    if ($fileObject instanceof FileReference) {
0 ignored issues
show
introduced by
$fileObject is never a sub-type of TYPO3\CMS\Core\Resource\FileReference.
Loading history...
4833
                        return $fileObject->getOriginalFile()->getUid();
4834
                    }
4835
                    return null;
4836
                case 'size':
4837
                    return $fileObject->getSize();
4838
                case 'sha1':
4839
                    return $fileObject->getSha1();
4840
                case 'extension':
4841
                    return $fileObject->getExtension();
4842
                case 'mimetype':
4843
                    return $fileObject->getMimeType();
4844
                case 'contents':
4845
                    return $fileObject->getContents();
4846
                case 'publicUrl':
4847
                    return $fileObject->getPublicUrl();
4848
                default:
4849
                    // Generic alternative here
4850
                    return $fileObject->getProperty($requestedFileInformationKey);
4851
            }
4852
        } else {
4853
            // @todo fail silently as is common in tslib_content
4854
            return 'Error: no file object';
4855
        }
4856
    }
4857
4858
    /**
4859
     * Returns a value from the current rootline (site) from $GLOBALS['TSFE']->tmpl->rootLine;
4860
     *
4861
     * @param string $key Which level in the root line
4862
     * @param string $field The field in the rootline record to return (a field from the pages table)
4863
     * @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
4864
     * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array
4865
     * @return string The value from the field of the rootline.
4866
     * @internal
4867
     * @see getData()
4868
     */
4869
    public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '')
4870
    {
4871
        $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine;
4872
        if (!$slideBack) {
4873
            return $rootLine[$key][$field];
4874
        }
4875
        for ($a = $key; $a >= 0; $a--) {
4876
            $val = $rootLine[$a][$field];
4877
            if ($val) {
4878
                return $val;
4879
            }
4880
        }
4881
4882
        return '';
4883
    }
4884
4885
    /**
4886
     * Return global variable where the input string $var defines array keys separated by "|"
4887
     * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
4888
     *
4889
     * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
4890
     * @param array $source Alternative array than $GLOBAL to get variables from.
4891
     * @return mixed Whatever value. If none, then blank string.
4892
     * @see getData()
4893
     */
4894
    public function getGlobal($keyString, $source = null)
4895
    {
4896
        $keys = explode('|', $keyString);
4897
        $numberOfLevels = count($keys);
4898
        $rootKey = trim($keys[0]);
4899
        $value = isset($source) ? $source[$rootKey] : $GLOBALS[$rootKey];
4900
        for ($i = 1; $i < $numberOfLevels && isset($value); $i++) {
4901
            $currentKey = trim($keys[$i]);
4902
            if (is_object($value)) {
4903
                $value = $value->{$currentKey};
4904
            } elseif (is_array($value)) {
4905
                $value = $value[$currentKey];
4906
            } else {
4907
                $value = '';
4908
                break;
4909
            }
4910
        }
4911
        if (!is_scalar($value)) {
4912
            $value = '';
4913
        }
4914
        return $value;
4915
    }
4916
4917
    /**
4918
     * 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).
4919
     * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost...
4920
     *
4921
     * @param int $key The integer to transform
4922
     * @param array $arr array in which the key should be found.
4923
     * @return int The processed integer key value.
4924
     * @internal
4925
     * @see getData()
4926
     */
4927
    public function getKey($key, $arr)
4928
    {
4929
        $key = (int)$key;
4930
        if (is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
4931
            if ($key < 0) {
4932
                $key = count($arr) + $key;
4933
            }
4934
            if ($key < 0) {
4935
                $key = 0;
4936
            }
4937
        }
4938
        return $key;
4939
    }
4940
4941
    /***********************************************
4942
     *
4943
     * Link functions (typolink)
4944
     *
4945
     ***********************************************/
4946
4947
    /**
4948
     * called from the typoLink() function
4949
     *
4950
     * does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1"
4951
     * into separate parts
4952
     *
4953
     * @param string $linkText The string (text) to link
4954
     * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
4955
     * @param array $configuration TypoScript configuration
4956
     * @return array|string
4957
     * @see typoLink()
4958
     *
4959
     * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links
4960
     */
4961
    protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
4962
    {
4963
        $linkParameter = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $linkParameter is dead and can be removed.
Loading history...
4964
4965
        // Link parameter value = first part
4966
        $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
4967
4968
        // Check for link-handler keyword
4969
        $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2);
4970
        $linkHandlerKeyword = $linkHandlerExploded[0] ?? null;
4971
        $linkHandlerValue = $linkHandlerExploded[1] ?? null;
4972
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword])
4973
            && (string)$linkHandlerValue !== ''
4974
        ) {
4975
            trigger_error('The hook $TYPO3_CONF_VARS[SC_OPTIONS][tslib/class.tslib_content.php][typolinkLinkHandler] will be removed in TYPO3 v11.0. Use a custom LinkHandler instead. The used link handler keyword was: ' . $linkHandlerKeyword, E_USER_DEPRECATED);
4976
            $linkHandlerObj = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword]);
4977
            if (method_exists($linkHandlerObj, 'main')) {
4978
                return $linkHandlerObj->main($linkText, $configuration, $linkHandlerKeyword, $linkHandlerValue, $mixedLinkParameter, $this);
4979
            }
4980
        }
4981
4982
        if (in_array(strtolower(preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
4983
            // Disallow insecure scheme's like javascript: or data:
4984
            return $linkText;
4985
        }
4986
        $linkParameter = $linkParameterParts['url'];
4987
4988
        // additional parameters that need to be set
4989
        if ($linkParameterParts['additionalParams'] !== '') {
4990
            $forceParams = $linkParameterParts['additionalParams'];
4991
            // params value
4992
            $configuration['additionalParams'] .= $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
4993
        }
4994
4995
        return [
4996
            'href'   => $linkParameter,
4997
            'target' => $linkParameterParts['target'],
4998
            'class'  => $linkParameterParts['class'],
4999
            'title'  => $linkParameterParts['title']
5000
        ];
5001
    }
5002
5003
    /**
5004
     * Implements the "typolink" property of stdWrap (and others)
5005
     * 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.
5006
     * 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.
5007
     * 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.
5008
     * For many more details on the parameters and how they are interpreted, please see the link to TSref below.
5009
     *
5010
     * the FAL API is handled with the namespace/prefix "file:..."
5011
     *
5012
     * @param string $linkText The string (text) to link
5013
     * @param array $conf TypoScript configuration (see link below)
5014
     * @return string A link-wrapped string.
5015
     * @see stdWrap()
5016
     * @see \TYPO3\CMS\Frontend\Plugin\AbstractPlugin::pi_linkTP()
5017
     */
5018
    public function typoLink($linkText, $conf)
5019
    {
5020
        $linkText = (string)$linkText;
5021
        $tsfe = $this->getTypoScriptFrontendController();
5022
5023
        $linkParameter = trim(
5024
            (isset($conf['parameter.']))
5025
            ? $this->stdWrap($conf['parameter'] ?? '', $conf['parameter.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
5026
            : ($conf['parameter'] ?? '')
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
5027
        );
5028
        $this->lastTypoLinkUrl = '';
5029
        $this->lastTypoLinkTarget = '';
5030
5031
        $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf);
5032
        // check if the link handler hook has resolved the link completely already
5033
        if (!is_array($resolvedLinkParameters)) {
5034
            return $resolvedLinkParameters;
5035
        }
5036
        $linkParameter = $resolvedLinkParameters['href'];
5037
        $target = $resolvedLinkParameters['target'];
5038
        $title = $resolvedLinkParameters['title'];
5039
5040
        if (!$linkParameter) {
5041
            return $this->resolveAnchorLink($linkText, $conf ?? []);
5042
        }
5043
5044
        // Detecting kind of link and resolve all necessary parameters
5045
        $linkService = GeneralUtility::makeInstance(LinkService::class);
5046
        try {
5047
            $linkDetails = $linkService->resolve($linkParameter);
5048
        } catch (UnknownLinkHandlerException | Exception\InvalidPathException $exception) {
5049
            $this->logger->warning('The link could not be generated', ['exception' => $exception]);
5050
            return $linkText;
5051
        }
5052
5053
        $linkDetails['typoLinkParameter'] = $linkParameter;
5054
        if (isset($linkDetails['type']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
5055
            /** @var AbstractTypolinkBuilder $linkBuilder */
5056
            $linkBuilder = GeneralUtility::makeInstance(
5057
                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
5058
                $this,
5059
                $tsfe
5060
            );
5061
            try {
5062
                [$this->lastTypoLinkUrl, $linkText, $target] = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
5063
                $this->lastTypoLinkTarget = htmlspecialchars($target);
5064
                $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
5065
                $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
5066
            } catch (UnableToLinkException $e) {
5067
                $this->logger->debug(sprintf('Unable to link "%s": %s', $e->getLinkText(), $e->getMessage()), ['exception' => $e]);
5068
5069
                // Only return the link text directly
5070
                return $e->getLinkText();
5071
            }
5072
        } elseif (isset($linkDetails['url'])) {
5073
            $this->lastTypoLinkUrl = $linkDetails['url'];
5074
            $this->lastTypoLinkTarget = htmlspecialchars($target);
5075
            $this->lastTypoLinkLD['target'] = htmlspecialchars($target);
5076
            $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
5077
        } else {
5078
            return $linkText;
5079
        }
5080
5081
        // We need to backup the URL because ATagParams might call typolink again and change the last URL.
5082
        $url = $this->lastTypoLinkUrl;
5083
        $finalTagParts = [
5084
            'aTagParams' => $this->getATagParams($conf) . $this->extLinkATagParams($this->lastTypoLinkUrl, $linkDetails['type']),
5085
            'url'        => $url,
5086
            'TYPE'       => $linkDetails['type']
5087
        ];
5088
5089
        // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
5090
        if (!empty($finalTagParts['aTagParams'])) {
5091
            $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']);
5092
            if (isset($aTagParams['href'])) {
5093
                unset($aTagParams['href']);
5094
                $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams);
5095
            }
5096
        }
5097
5098
        // Building the final <a href=".."> tag
5099
        $tagAttributes = [];
5100
5101
        // Title attribute
5102
        if (empty($title)) {
5103
            $title = $conf['title'] ?? '';
5104
            if (isset($conf['title.']) && is_array($conf['title.'])) {
5105
                $title = $this->stdWrap($title, $conf['title.']);
5106
            }
5107
        }
5108
5109
        // Check, if the target is coded as a JS open window link:
5110
        $JSwindowParts = [];
5111
        $JSwindowParams = '';
5112
        if ($target && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $target, $JSwindowParts)) {
5113
            // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
5114
            $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower(($conf['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
5115
            $JSwindow_paramsArr = [];
5116
            $target = $conf['target'] ?? 'FEopenLink';
5117
            foreach ($JSwindow_tempParamsArr as $JSv) {
5118
                [$JSp, $JSv] = explode('=', $JSv, 2);
5119
                // If the target is set as JS param, this is extracted
5120
                if ($JSp === 'target') {
5121
                    $target = $JSv;
5122
                } else {
5123
                    $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
5124
                }
5125
            }
5126
            // Add width/height:
5127
            $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
5128
            $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
5129
            // Imploding into string:
5130
            $JSwindowParams = implode(',', $JSwindow_paramsArr);
5131
        }
5132
        if (!$JSwindowParams && $linkDetails['type'] === LinkService::TYPE_EMAIL && $tsfe->spamProtectEmailAddresses === 'ascii') {
5133
            $tagAttributes['href'] = $finalTagParts['url'];
5134
        } else {
5135
            $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']);
5136
        }
5137
        if (!empty($title)) {
5138
            $tagAttributes['title'] = htmlspecialchars($title);
5139
        }
5140
5141
        // Target attribute
5142
        if (!empty($target)) {
5143
            $tagAttributes['target'] = htmlspecialchars($target);
5144
        }
5145
        if ($JSwindowParams && in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
5146
            // Create TARGET-attribute only if the right doctype is used
5147
            unset($tagAttributes['target']);
5148
        }
5149
5150
        if ($JSwindowParams) {
5151
            $onClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($finalTagParts['url']))
5152
                . ',' . GeneralUtility::quoteJSvalue($target) . ','
5153
                . GeneralUtility::quoteJSvalue($JSwindowParams)
5154
                . ');vHWin.focus();return false;';
5155
            $tagAttributes['onclick'] = htmlspecialchars($onClick);
5156
        }
5157
5158
        if (!empty($resolvedLinkParameters['class'])) {
5159
            $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']);
5160
        }
5161
5162
        // Prevent trouble with double and missing spaces between attributes and merge params before implode
5163
        $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
5164
        $finalTagAttributes = $this->addSecurityRelValues($finalTagAttributes, $target, $tagAttributes['href']);
5165
        $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
5166
5167
        if (!empty($finalTagParts['aTagParams'])) {
5168
            $tagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
5169
        }
5170
        // kept for backwards-compatibility in hooks
5171
        $finalTagParts['targetParams'] = !empty($tagAttributes['target']) ? ' target="' . $tagAttributes['target'] . '"' : '';
5172
        $this->lastTypoLinkTarget = $target;
5173
5174
        // Call user function:
5175
        if ($conf['userFunc'] ?? false) {
5176
            $finalTagParts['TAG'] = $finalAnchorTag;
5177
            $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $finalTagParts);
0 ignored issues
show
Bug introduced by
$finalTagParts of type array<string,mixed|string> is incompatible with the type string expected by parameter $content of TYPO3\CMS\Frontend\Conte...rer::callUserFunction(). ( Ignorable by Annotation )

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

5177
            $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], /** @scrutinizer ignore-type */ $finalTagParts);
Loading history...
5178
        }
5179
5180
        // Hook: Call post processing function for link rendering:
5181
        $_params = [
5182
            'conf' => &$conf,
5183
            'linktxt' => &$linkText,
5184
            'finalTag' => &$finalAnchorTag,
5185
            'finalTagParts' => &$finalTagParts,
5186
            'linkDetails' => &$linkDetails,
5187
            'tagAttributes' => &$tagAttributes
5188
        ];
5189
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) {
5190
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
5191
            GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
5192
        }
5193
5194
        // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made:
5195
        if ($conf['returnLast'] ?? false) {
5196
            switch ($conf['returnLast']) {
5197
                case 'url':
5198
                    return $this->lastTypoLinkUrl;
5199
                case 'target':
5200
                    return $this->lastTypoLinkTarget;
5201
            }
5202
        }
5203
5204
        $wrap = isset($conf['wrap.'])
5205
            ? $this->stdWrap($conf['wrap'] ?? '', $conf['wrap.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
5206
            : $conf['wrap'] ?? '';
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
5207
5208
        if ($conf['ATagBeforeWrap'] ?? false) {
5209
            return $finalAnchorTag . $this->wrap($linkText, $wrap) . '</a>';
5210
        }
5211
        return $this->wrap($finalAnchorTag . $linkText . '</a>', $wrap);
5212
    }
5213
5214
    protected function addSecurityRelValues(array $tagAttributes, ?string $target, string $url): array
5215
    {
5216
        $relAttribute = 'noreferrer';
5217
        if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($url)) {
5218
            return $tagAttributes;
5219
        }
5220
5221
        if (!isset($tagAttributes['rel'])) {
5222
            $tagAttributes['rel'] = $relAttribute;
5223
            return $tagAttributes;
5224
        }
5225
5226
        $tagAttributes['rel'] = implode(' ', array_unique(array_merge(
5227
            GeneralUtility::trimExplode(' ', $relAttribute),
5228
            GeneralUtility::trimExplode(' ', $tagAttributes['rel'])
5229
        )));
5230
5231
        return $tagAttributes;
5232
    }
5233
5234
    /**
5235
     * Checks whether the given url is an internal url.
5236
     *
5237
     * It will check the host part only, against all configured sites
5238
     * whether the given host is any. If so, the url is considered internal
5239
     *
5240
     * @param string $url The url to check.
5241
     * @return bool
5242
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
5243
     */
5244
    protected function isInternalUrl(string $url): bool
5245
    {
5246
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
5247
        $parsedUrl = parse_url($url);
5248
        $foundDomains = 0;
5249
        if (!isset($parsedUrl['host'])) {
5250
            return true;
5251
        }
5252
5253
        $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
5254
5255
        if ($cache->has($cacheIdentifier) === false) {
5256
            foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
5257
                if ($site->getBase()->getHost() === $parsedUrl['host']) {
5258
                    ++$foundDomains;
5259
                    break;
5260
                }
5261
5262
                if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url)) {
5263
                    ++$foundDomains;
5264
                    break;
5265
                }
5266
            }
5267
5268
            $cache->set($cacheIdentifier, $foundDomains > 0);
5269
        }
5270
5271
        return (bool)$cache->get($cacheIdentifier);
5272
    }
5273
5274
    /**
5275
     * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
5276
     *
5277
     * @param array $conf TypoScript properties for "typolink
5278
     * @return string The URL of the link-tag that typolink() would by itself return
5279
     * @see typoLink()
5280
     */
5281
    public function typoLink_URL($conf)
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::typoLink_URL" is not in camel caps format
Loading history...
5282
    {
5283
        $this->typoLink('|', $conf);
5284
        return $this->lastTypoLinkUrl;
5285
    }
5286
5287
    /**
5288
     * Returns a linked string made from typoLink parameters.
5289
     *
5290
     * 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.
5291
     * Optionally you can supply $urlParameters which is an array with key/value pairs that are rawurlencoded and appended to the resulting url.
5292
     *
5293
     * @param string $label Text string being wrapped by the link.
5294
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
5295
     * @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.
5296
     * @param string $target Specific target set, if any. (Default is using the current)
5297
     * @return string The wrapped $label-text string
5298
     * @see getTypoLink_URL()
5299
     */
5300
    public function getTypoLink($label, $params, $urlParameters = [], $target = '')
5301
    {
5302
        $conf = [];
5303
        $conf['parameter'] = $params;
5304
        if ($target) {
5305
            $conf['target'] = $target;
5306
            $conf['extTarget'] = $target;
5307
            $conf['fileTarget'] = $target;
5308
        }
5309
        if (is_array($urlParameters)) {
5310
            if (!empty($urlParameters)) {
5311
                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
5312
            }
5313
        } else {
5314
            $conf['additionalParams'] .= $urlParameters;
5315
        }
5316
        $out = $this->typoLink($label, $conf);
5317
        return $out;
5318
    }
5319
5320
    /**
5321
     * Returns the canonical URL to the current "location", which include the current page ID and type
5322
     * and optionally the query string
5323
     *
5324
     * @param bool $addQueryString Whether additional GET arguments in the query string should be included or not
5325
     * @return string
5326
     */
5327
    public function getUrlToCurrentLocation($addQueryString = true)
5328
    {
5329
        $conf = [];
5330
        $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type;
5331
        if ($addQueryString) {
5332
            $conf['addQueryString'] = '1';
5333
            $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars)));
5334
            $conf['addQueryString.'] = [
5335
                'method' => 'GET',
5336
                'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : '')
5337
            ];
5338
        }
5339
5340
        return $this->typoLink_URL($conf);
5341
    }
5342
5343
    /**
5344
     * Returns the URL of a "typolink" create from the input parameter string, url-parameters and target
5345
     *
5346
     * @param string $params Link parameter; eg. "123" for page id, "[email protected]" for email address, "http://...." for URL, "fileadmin/example.txt" for file.
5347
     * @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.
5348
     * @param string $target Specific target set, if any. (Default is using the current)
5349
     * @return string The URL
5350
     * @see getTypoLink()
5351
     */
5352
    public function getTypoLink_URL($params, $urlParameters = [], $target = '')
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::getTypoLink_URL" is not in camel caps format
Loading history...
5353
    {
5354
        $this->getTypoLink('', $params, $urlParameters, $target);
5355
        return $this->lastTypoLinkUrl;
5356
    }
5357
5358
    /**
5359
     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
5360
     *
5361
     * @param string $context The context in which the method is called (e.g. typoLink).
5362
     * @param string $url The URL that should be processed.
5363
     * @param array $typolinkConfiguration The current link configuration array.
5364
     * @return string|null Returns NULL if URL was not processed or the processed URL as a string.
5365
     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
5366
     */
5367
    protected function processUrl($context, $url, $typolinkConfiguration = [])
5368
    {
5369
        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
5370
        if (empty($urlProcessors)) {
5371
            return $url;
5372
        }
5373
5374
        foreach ($urlProcessors as $identifier => $configuration) {
5375
            if (empty($configuration) || !is_array($configuration)) {
5376
                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
5377
            }
5378
            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
5379
                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
5380
            }
5381
        }
5382
5383
        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
5384
        $keepProcessing = true;
5385
5386
        foreach ($orderedProcessors as $configuration) {
5387
            /** @var UrlProcessorInterface $urlProcessor */
5388
            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
5389
            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
5390
            if (!$keepProcessing) {
5391
                break;
5392
            }
5393
        }
5394
5395
        return $url;
5396
    }
5397
5398
    /**
5399
     * Creates a href attibute for given $mailAddress.
5400
     * The function uses spamProtectEmailAddresses for encoding the mailto statement.
5401
     * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:[email protected]".
5402
     *
5403
     * @param string $mailAddress Email address
5404
     * @param string $linktxt Link text, default will be the email address.
5405
     * @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.
5406
     */
5407
    public function getMailTo($mailAddress, $linktxt)
5408
    {
5409
        $mailAddress = (string)$mailAddress;
5410
        if ((string)$linktxt === '') {
5411
            $linktxt = htmlspecialchars($mailAddress);
5412
        }
5413
5414
        $originalMailToUrl = 'mailto:' . $mailAddress;
5415
        $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
5416
5417
        // no processing happened, therefore, the default processing kicks in
5418
        if ($mailToUrl === $originalMailToUrl) {
5419
            $tsfe = $this->getTypoScriptFrontendController();
5420
            if ($tsfe->spamProtectEmailAddresses) {
5421
                $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
5422
                if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
5423
                    $encodedForJsAndHref = rawurlencode(GeneralUtility::quoteJSvalue($mailToUrl));
5424
                    $mailToUrl = 'javascript:linkTo_UnCryptMailto(' . $encodedForJsAndHref . ');';
5425
                }
5426
                $atLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst']) ?: '(at)';
5427
                $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
5428
                if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']) {
5429
                    $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
5430
                    $lastDotLabel = $lastDotLabel ?: '(dot)';
5431
                    $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
5432
                }
5433
                $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
5434
            }
5435
        }
5436
5437
        return [$mailToUrl, $linktxt];
5438
    }
5439
5440
    /**
5441
     * Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
5442
     *
5443
     * @param string $string Input string to en/decode: "mailto:[email protected]
5444
     * @param mixed  $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses
5445
     * @return string encoded version of $string
5446
     */
5447
    protected function encryptEmail(string $string, $type): string
5448
    {
5449
        $out = '';
5450
        // obfuscates using the decimal HTML entity references for each character
5451
        if ($type === 'ascii') {
5452
            foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5453
                $out .= '&#' . mb_ord($char) . ';';
0 ignored issues
show
Bug introduced by
The call to mb_ord() has too few arguments starting with encoding. ( Ignorable by Annotation )

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

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

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

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

Loading history...
5454
            }
5455
        } else {
5456
            // like str_rot13() but with a variable offset and a wider character range
5457
            $len = strlen($string);
5458
            $offset = (int)$type;
5459
            for ($i = 0; $i < $len; $i++) {
5460
                $charValue = ord($string[$i]);
5461
                // 0-9 . , - + / :
5462
                if ($charValue >= 43 && $charValue <= 58) {
5463
                    $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5464
                } elseif ($charValue >= 64 && $charValue <= 90) {
5465
                    // A-Z @
5466
                    $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5467
                } elseif ($charValue >= 97 && $charValue <= 122) {
5468
                    // a-z
5469
                    $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5470
                } else {
5471
                    $out .= $string[$i];
5472
                }
5473
            }
5474
        }
5475
        return $out;
5476
    }
5477
5478
    /**
5479
     * Encryption (or decryption) of a single character.
5480
     * Within the given range the character is shifted with the supplied offset.
5481
     *
5482
     * @param int $n Ordinal of input character
5483
     * @param int $start Start of range
5484
     * @param int $end End of range
5485
     * @param int $offset Offset
5486
     * @return string encoded/decoded version of character
5487
     */
5488
    protected function encryptCharcode($n, $start, $end, $offset)
5489
    {
5490
        $n = $n + $offset;
5491
        if ($offset > 0 && $n > $end) {
5492
            $n = $start + ($n - $end - 1);
5493
        } elseif ($offset < 0 && $n < $start) {
5494
            $n = $end - ($start - $n - 1);
5495
        }
5496
        return chr($n);
5497
    }
5498
5499
    /**
5500
     * Gets the query arguments and assembles them for URLs.
5501
     * Arguments may be removed or set, depending on configuration.
5502
     *
5503
     * @param array $conf Configuration
5504
     * @param array $overruleQueryArguments Multidimensional key/value pairs that overrule incoming query arguments
5505
     * @param bool $forceOverruleArguments If set, key/value pairs not in the query but the overrule array will be set
5506
     * @return string The URL query part (starting with a &)
5507
     */
5508
    public function getQueryArguments($conf, $overruleQueryArguments = [], $forceOverruleArguments = false)
5509
    {
5510
        $exclude = [];
5511
        $method = (string)($conf['method'] ?? '');
5512
        if ($method === 'POST') {
5513
            trigger_error('Assigning typolink.addQueryString.method = POST is not supported anymore since TYPO3 v10.0.', E_USER_WARNING);
5514
            return '';
5515
        }
5516
        if ($method === 'GET,POST' || $method === 'POST,GET') {
5517
            trigger_error('Assigning typolink.addQueryString.method = GET,POST or POST,GET is not supported anymore since TYPO3 v10.0 - falling back to GET.', E_USER_WARNING);
5518
            $method = 'GET';
5519
        }
5520
        if ($method === 'GET') {
5521
            $currentQueryArray = GeneralUtility::_GET();
5522
        } else {
5523
            $currentQueryArray = [];
5524
            parse_str($this->getEnvironmentVariable('QUERY_STRING'), $currentQueryArray);
5525
        }
5526
        if ($conf['exclude'] ?? false) {
5527
            $excludeString = str_replace(',', '&', $conf['exclude']);
5528
            $excludedQueryParts = [];
5529
            parse_str($excludeString, $excludedQueryParts);
5530
            // never repeat id
5531
            $exclude['id'] = 0;
5532
            $newQueryArray = ArrayUtility::arrayDiffAssocRecursive($currentQueryArray, $excludedQueryParts);
5533
        } else {
5534
            $newQueryArray = $currentQueryArray;
5535
        }
5536
        ArrayUtility::mergeRecursiveWithOverrule($newQueryArray, $overruleQueryArguments, $forceOverruleArguments);
5537
        return HttpUtility::buildQueryString($newQueryArray, '&');
5538
    }
5539
5540
    /***********************************************
5541
     *
5542
     * Miscellaneous functions, stand alone
5543
     *
5544
     ***********************************************/
5545
    /**
5546
     * Wrapping a string.
5547
     * Implements the TypoScript "wrap" property.
5548
     * Example: $content = "HELLO WORLD" and $wrap = "<strong> | </strong>", result: "<strong>HELLO WORLD</strong>"
5549
     *
5550
     * @param string $content The content to wrap
5551
     * @param string $wrap The wrap value, eg. "<strong> | </strong>
5552
     * @param string $char The char used to split the wrapping value, default is "|
5553
     * @return string Wrapped input string
5554
     * @see noTrimWrap()
5555
     */
5556
    public function wrap($content, $wrap, $char = '|')
5557
    {
5558
        if ($wrap) {
5559
            $wrapArr = explode($char, $wrap);
5560
            $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
5561
        }
5562
        return $content;
5563
    }
5564
5565
    /**
5566
     * Wrapping a string, preserving whitespace in wrap value.
5567
     * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does)
5568
     *
5569
     * @param string $content The content to wrap, eg. "HELLO WORLD
5570
     * @param string $wrap The wrap value, eg. " | <strong> | </strong>
5571
     * @param string $char The char used to split the wrapping value, default is "|"
5572
     * @return string Wrapped input string, eg. " <strong> HELLO WORD </strong>
5573
     * @see wrap()
5574
     */
5575
    public function noTrimWrap($content, $wrap, $char = '|')
5576
    {
5577
        if ($wrap) {
5578
            // expects to be wrapped with (at least) 3 characters (before, middle, after)
5579
            // anything else is not taken into account
5580
            $wrapArr = explode($char, $wrap, 4);
5581
            $content = $wrapArr[1] . $content . $wrapArr[2];
5582
        }
5583
        return $content;
5584
    }
5585
5586
    /**
5587
     * Calling a user function/class-method
5588
     * Notice: For classes the instantiated object will have the internal variable, $cObj, set to be a *reference* to $this (the parent/calling object).
5589
     *
5590
     * @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.
5591
     * @param array $conf The TypoScript configuration to pass the function
5592
     * @param string $content The content string to pass the function
5593
     * @return string The return content from the function call. Should probably be a string.
5594
     * @see stdWrap()
5595
     * @see typoLink()
5596
     * @see _parseFunc()
5597
     */
5598
    public function callUserFunction($funcName, $conf, $content)
5599
    {
5600
        // Split parts
5601
        $parts = explode('->', $funcName);
5602
        if (count($parts) === 2) {
5603
            // Check whether PHP class is available
5604
            if (class_exists($parts[0])) {
5605
                if ($this->container && $this->container->has($parts[0])) {
5606
                    $classObj = $this->container->get($parts[0]);
5607
                } else {
5608
                    $classObj = GeneralUtility::makeInstance($parts[0]);
5609
                }
5610
                if (is_object($classObj) && method_exists($classObj, $parts[1])) {
5611
                    $classObj->cObj = $this;
5612
                    $content = call_user_func_array([
5613
                        $classObj,
5614
                        $parts[1]
5615
                    ], [
5616
                        $content,
5617
                        $conf
5618
                    ]);
5619
                } else {
5620
                    $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', 3);
5621
                }
5622
            } else {
5623
                $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', 3);
5624
            }
5625
        } elseif (function_exists($funcName)) {
5626
            $content = call_user_func($funcName, $content, $conf);
5627
        } else {
5628
            $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', 3);
5629
        }
5630
        return $content;
5631
    }
5632
5633
    /**
5634
     * Cleans up a string of keywords. Keywords at splitted by "," (comma)  ";" (semi colon) and linebreak
5635
     *
5636
     * @param string $content String of keywords
5637
     * @return string Cleaned up string, keywords will be separated by a comma only.
5638
     */
5639
    public function keywords($content)
5640
    {
5641
        $listArr = preg_split('/[,;' . LF . ']/', $content);
5642
        foreach ($listArr as $k => $v) {
5643
            $listArr[$k] = trim($v);
5644
        }
5645
        return implode(',', $listArr);
0 ignored issues
show
Bug introduced by
It seems like $listArr can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

5645
        return implode(',', /** @scrutinizer ignore-type */ $listArr);
Loading history...
5646
    }
5647
5648
    /**
5649
     * Changing character case of a string, converting typically used western charset characters as well.
5650
     *
5651
     * @param string $theValue The string to change case for.
5652
     * @param string $case The direction; either "upper" or "lower
5653
     * @return string
5654
     * @see HTMLcaseshift()
5655
     */
5656
    public function caseshift($theValue, $case)
5657
    {
5658
        switch (strtolower($case)) {
5659
            case 'upper':
5660
                $theValue = mb_strtoupper($theValue, 'utf-8');
5661
                break;
5662
            case 'lower':
5663
                $theValue = mb_strtolower($theValue, 'utf-8');
5664
                break;
5665
            case 'capitalize':
5666
                $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8');
5667
                break;
5668
            case 'ucfirst':
5669
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5670
                $firstChar = mb_strtoupper($firstChar, 'utf-8');
5671
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5672
                $theValue = $firstChar . $remainder;
5673
                break;
5674
            case 'lcfirst':
5675
                $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5676
                $firstChar = mb_strtolower($firstChar, 'utf-8');
5677
                $remainder = mb_substr($theValue, 1, null, 'utf-8');
5678
                $theValue = $firstChar . $remainder;
5679
                break;
5680
            case 'uppercamelcase':
5681
                $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue);
5682
                break;
5683
            case 'lowercamelcase':
5684
                $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue);
5685
                break;
5686
        }
5687
        return $theValue;
5688
    }
5689
5690
    /**
5691
     * Shifts the case of characters outside of HTML tags in the input string
5692
     *
5693
     * @param string $theValue The string to change case for.
5694
     * @param string $case The direction; either "upper" or "lower
5695
     * @return string
5696
     * @see caseshift()
5697
     */
5698
    public function HTMLcaseshift($theValue, $case)
5699
    {
5700
        $inside = 0;
5701
        $newVal = '';
5702
        $pointer = 0;
5703
        $totalLen = strlen($theValue);
5704
        do {
5705
            if (!$inside) {
5706
                $len = strcspn(substr($theValue, $pointer), '<');
5707
                $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case);
5708
                $inside = 1;
5709
            } else {
5710
                $len = strcspn(substr($theValue, $pointer), '>') + 1;
5711
                $newVal .= substr($theValue, $pointer, $len);
5712
                $inside = 0;
5713
            }
5714
            $pointer += $len;
5715
        } while ($pointer < $totalLen);
5716
        return $newVal;
5717
    }
5718
5719
    /**
5720
     * Returns the 'age' of the tstamp $seconds
5721
     *
5722
     * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs
5723
     * @param string $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs'
5724
     * @return string The formatted string
5725
     */
5726
    public function calcAge($seconds, $labels)
5727
    {
5728
        if (MathUtility::canBeInterpretedAsInteger($labels)) {
5729
            $labels = ' min| hrs| days| yrs| min| hour| day| year';
5730
        } else {
5731
            $labels = str_replace('"', '', $labels);
5732
        }
5733
        $labelArr = explode('|', $labels);
5734
        if (count($labelArr) === 4) {
5735
            $labelArr = array_merge($labelArr, $labelArr);
5736
        }
5737
        $absSeconds = abs($seconds);
5738
        $sign = $seconds > 0 ? 1 : -1;
5739
        if ($absSeconds < 3600) {
5740
            $val = round($absSeconds / 60);
5741
            $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]);
5742
        } elseif ($absSeconds < 24 * 3600) {
5743
            $val = round($absSeconds / 3600);
5744
            $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]);
5745
        } elseif ($absSeconds < 365 * 24 * 3600) {
5746
            $val = round($absSeconds / (24 * 3600));
5747
            $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]);
5748
        } else {
5749
            $val = round($absSeconds / (365 * 24 * 3600));
5750
            $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null));
5751
        }
5752
        return $seconds;
5753
    }
5754
5755
    /**
5756
     * Sends a notification email
5757
     *
5758
     * @param string $message The message content. If blank, no email is sent.
5759
     * @param string $recipients Comma list of recipient email addresses
5760
     * @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.
5761
     * @param string $senderAddress "From" email address
5762
     * @param string $senderName Optional "From" name
5763
     * @param string $replyTo Optional "Reply-To" header email address.
5764
     * @return bool Returns TRUE if sent
5765
     * @deprecated ContentObjectRenderer::sendNotifyEmail is deprecated and will be removed in TYPO3 v11. Consider using the mail API directly
5766
     */
5767
    public function sendNotifyEmail($message, $recipients, $cc, $senderAddress, $senderName = '', $replyTo = '')
5768
    {
5769
        trigger_error('ContentObjectRenderer::sendNotifyEmail is deprecated and will be removed in TYPO3 v11. Consider using the mail API directly.', E_USER_DEPRECATED);
5770
        /** @var MailMessage $mail */
5771
        $mail = GeneralUtility::makeInstance(MailMessage::class);
5772
        $senderName = trim($senderName);
5773
        $senderAddress = trim($senderAddress);
5774
        if ($senderAddress !== '') {
5775
            $mail->from(new Address($senderAddress, $senderName));
5776
        }
5777
        $parsedReplyTo = MailUtility::parseAddresses($replyTo);
5778
        if (!empty($parsedReplyTo)) {
5779
            $mail->replyTo($parsedReplyTo);
0 ignored issues
show
Bug introduced by
$parsedReplyTo of type array is incompatible with the type Symfony\Component\Mime\Address|string expected by parameter $addresses of Symfony\Component\Mime\Email::replyTo(). ( Ignorable by Annotation )

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

5779
            $mail->replyTo(/** @scrutinizer ignore-type */ $parsedReplyTo);
Loading history...
5780
        }
5781
        $message = trim($message);
5782
        if ($message !== '') {
5783
            // First line is subject
5784
            $messageParts = explode(LF, $message, 2);
5785
            $subject = trim($messageParts[0]);
5786
            $plainMessage = trim($messageParts[1]);
5787
            $parsedRecipients = MailUtility::parseAddresses($recipients);
5788
            if (!empty($parsedRecipients)) {
5789
                $mail->to(...$parsedRecipients)
5790
                    ->subject($subject)
5791
                    ->text($plainMessage);
5792
                $mail->send();
5793
            }
5794
            $parsedCc = MailUtility::parseAddresses($cc);
5795
            if (!empty($parsedCc)) {
5796
                $from = $mail->getFrom();
5797
                /** @var MailMessage $mail */
5798
                $mail = GeneralUtility::makeInstance(MailMessage::class);
5799
                if (!empty($parsedReplyTo)) {
5800
                    $mail->replyTo($parsedReplyTo);
5801
                }
5802
                $mail->from($from)
0 ignored issues
show
Bug introduced by
$from of type array is incompatible with the type Symfony\Component\Mime\Address|string expected by parameter $addresses of Symfony\Component\Mime\Email::from(). ( Ignorable by Annotation )

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

5802
                $mail->from(/** @scrutinizer ignore-type */ $from)
Loading history...
5803
                    ->to(...$parsedCc)
5804
                    ->subject($subject)
5805
                    ->text($plainMessage);
5806
                $mail->send();
5807
            }
5808
            return true;
5809
        }
5810
        return false;
5811
    }
5812
5813
    /**
5814
     * Resolves a TypoScript reference value to the full set of properties BUT overridden with any local properties set.
5815
     * So the reference is resolved but overlaid with local TypoScript properties of the reference value.
5816
     *
5817
     * @param array $confArr The TypoScript array
5818
     * @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.
5819
     * @return array The modified TypoScript array
5820
     */
5821
    public function mergeTSRef($confArr, $prop)
5822
    {
5823
        if ($confArr[$prop][0] === '<') {
5824
            $key = trim(substr($confArr[$prop], 1));
5825
            $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
5826
            // $name and $conf is loaded with the referenced values.
5827
            $old_conf = $confArr[$prop . '.'];
5828
            [, $conf] = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
5829
            if (is_array($old_conf) && !empty($old_conf)) {
5830
                $conf = is_array($conf) ? array_replace_recursive($conf, $old_conf) : $old_conf;
5831
            }
5832
            $confArr[$prop . '.'] = $conf;
5833
        }
5834
        return $confArr;
5835
    }
5836
5837
    /***********************************************
5838
     *
5839
     * Database functions, making of queries
5840
     *
5841
     ***********************************************/
5842
5843
    /**
5844
     * Generates a list of Page-uid's from $id. List does not include $id itself
5845
     * (unless the id specified is negative in which case it does!)
5846
     * The only pages WHICH PREVENTS DECENDING in a branch are
5847
     * - deleted pages,
5848
     * - pages in a recycler (doktype = 255) or of the Backend User Section (doktpe = 6) type
5849
     * - pages that has the extendToSubpages set, WHERE start/endtime, hidden
5850
     * and fe_users would hide the records.
5851
     * Apart from that, pages with enable-fields excluding them, will also be
5852
     * removed. HOWEVER $dontCheckEnableFields set will allow
5853
     * enableFields-excluded pages to be included anyway - including
5854
     * extendToSubpages sections!
5855
     * Mount Pages are also descended but notice that these ID numbers are not
5856
     * useful for links unless the correct MPvar is set.
5857
     *
5858
     * @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!
5859
     * @param int $depth The number of levels to descend. If you want to descend infinitely, just set this to 100 or so. Should be at least "1" since zero will just make the function return (no descend...)
5860
     * @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'
5861
     * @param bool $dontCheckEnableFields See function description
5862
     * @param string $addSelectFields Additional fields to select. Syntax: ",[fieldname],[fieldname],...
5863
     * @param string $moreWhereClauses Additional where clauses. Syntax: " AND [fieldname]=[value] AND ...
5864
     * @param array $prevId_array array of IDs from previous recursions. In order to prevent infinite loops with mount pages.
5865
     * @param int $recursionLevel Internal: Zero for the first recursion, incremented for each recursive call.
5866
     * @return string Returns the list of ids as a comma separated string
5867
     * @see TypoScriptFrontendController::checkEnableFields()
5868
     * @see TypoScriptFrontendController::checkPagerecordForIncludeSection()
5869
     */
5870
    public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0)
5871
    {
5872
        $id = (int)$id;
5873
        if (!$id) {
5874
            return '';
5875
        }
5876
5877
        // Init vars:
5878
        $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state' . $addSelectFields;
5879
        $depth = (int)$depth;
5880
        $begin = (int)$begin;
5881
        $theList = [];
5882
        $addId = 0;
5883
        $requestHash = '';
5884
5885
        // First level, check id (second level, this is done BEFORE the recursive call)
5886
        $tsfe = $this->getTypoScriptFrontendController();
5887
        if (!$recursionLevel) {
5888
            // Check tree list cache
5889
            // First, create the hash for this request - not sure yet whether we need all these parameters though
5890
            $parameters = [
5891
                $id,
5892
                $depth,
5893
                $begin,
5894
                $dontCheckEnableFields,
5895
                $addSelectFields,
5896
                $moreWhereClauses,
5897
                $prevId_array,
5898
                GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1])
5899
            ];
5900
            $requestHash = md5(serialize($parameters));
5901
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5902
                ->getQueryBuilderForTable('cache_treelist');
5903
            $cacheEntry = $queryBuilder->select('treelist')
5904
                ->from('cache_treelist')
5905
                ->where(
5906
                    $queryBuilder->expr()->eq(
5907
                        'md5hash',
5908
                        $queryBuilder->createNamedParameter($requestHash, \PDO::PARAM_STR)
5909
                    ),
5910
                    $queryBuilder->expr()->orX(
5911
                        $queryBuilder->expr()->gt(
5912
                            'expires',
5913
                            $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
5914
                        ),
5915
                        $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5916
                    )
5917
                )
5918
                ->setMaxResults(1)
5919
                ->execute()
5920
                ->fetch();
5921
5922
            if (is_array($cacheEntry)) {
5923
                // Cache hit
5924
                return $cacheEntry['treelist'];
5925
            }
5926
            // If Id less than zero it means we should add the real id to list:
5927
            if ($id < 0) {
5928
                $addId = $id = abs($id);
5929
            }
5930
            // Check start page:
5931
            if ($tsfe->sys_page->getRawRecord('pages', $id, 'uid')) {
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type double; however, parameter $uid of TYPO3\CMS\Core\Domain\Re...ository::getRawRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

5931
            if ($tsfe->sys_page->getRawRecord('pages', /** @scrutinizer ignore-type */ $id, 'uid')) {
Loading history...
5932
                // Find mount point if any:
5933
                $mount_info = $tsfe->sys_page->getMountPointInfo($id);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type double; however, parameter $pageId of TYPO3\CMS\Core\Domain\Re...ry::getMountPointInfo() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

5933
                $mount_info = $tsfe->sys_page->getMountPointInfo(/** @scrutinizer ignore-type */ $id);
Loading history...
5934
                if (is_array($mount_info)) {
5935
                    $id = $mount_info['mount_pid'];
5936
                    // In Overlay mode, use the mounted page uid as added ID!:
5937
                    if ($addId && $mount_info['overlay']) {
5938
                        $addId = $id;
5939
                    }
5940
                }
5941
            } else {
5942
                // Return blank if the start page was NOT found at all!
5943
                return '';
5944
            }
5945
        }
5946
        // Add this ID to the array of IDs
5947
        if ($begin <= 0) {
5948
            $prevId_array[] = $id;
5949
        }
5950
        // Select sublevel:
5951
        if ($depth > 0) {
5952
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
5953
            $queryBuilder->getRestrictions()
5954
                ->removeAll()
5955
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5956
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5957
                ->from('pages')
5958
                ->where(
5959
                    $queryBuilder->expr()->eq(
5960
                        'pid',
5961
                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
5962
                    ),
5963
                    // tree is only built by language=0 pages
5964
                    $queryBuilder->expr()->eq('sys_language_uid', 0)
5965
                )
5966
                ->orderBy('sorting');
5967
5968
            if (!empty($moreWhereClauses)) {
5969
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5970
            }
5971
5972
            $result = $queryBuilder->execute();
5973
            while ($row = $result->fetch()) {
5974
                /** @var VersionState $versionState */
5975
                $versionState = VersionState::cast($row['t3ver_state']);
5976
                $tsfe->sys_page->versionOL('pages', $row);
5977
                if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5978
                    || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5979
                    || $versionState->indicatesPlaceholder()
5980
                ) {
5981
                    // Doing this after the overlay to make sure changes
5982
                    // in the overlay are respected.
5983
                    // However, we do not process pages below of and
5984
                    // including of type recycler and BE user section
5985
                    continue;
5986
                }
5987
                // Find mount point if any:
5988
                $next_id = $row['uid'];
5989
                $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row);
5990
                // Overlay mode:
5991
                if (is_array($mount_info) && $mount_info['overlay']) {
5992
                    $next_id = $mount_info['mount_pid'];
5993
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5994
                        ->getQueryBuilderForTable('pages');
5995
                    $queryBuilder->getRestrictions()
5996
                        ->removeAll()
5997
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5998
                    $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5999
                        ->from('pages')
6000
                        ->where(
6001
                            $queryBuilder->expr()->eq(
6002
                                'uid',
6003
                                $queryBuilder->createNamedParameter($next_id, \PDO::PARAM_INT)
6004
                            )
6005
                        )
6006
                        ->orderBy('sorting')
6007
                        ->setMaxResults(1);
6008
6009
                    if (!empty($moreWhereClauses)) {
6010
                        $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
6011
                    }
6012
6013
                    $row = $queryBuilder->execute()->fetch();
6014
                    $tsfe->sys_page->versionOL('pages', $row);
6015
                    if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
6016
                        || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
6017
                        || $versionState->indicatesPlaceholder()
6018
                    ) {
6019
                        // Doing this after the overlay to make sure
6020
                        // changes in the overlay are respected.
6021
                        // see above
6022
                        continue;
6023
                    }
6024
                }
6025
                // Add record:
6026
                if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) {
6027
                    // Add ID to list:
6028
                    if ($begin <= 0) {
6029
                        if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) {
6030
                            $theList[] = $next_id;
6031
                        }
6032
                    }
6033
                    // Next level:
6034
                    if ($depth > 1 && !$row['php_tree_stop']) {
6035
                        // Normal mode:
6036
                        if (is_array($mount_info) && !$mount_info['overlay']) {
6037
                            $next_id = $mount_info['mount_pid'];
6038
                        }
6039
                        // Call recursively, if the id is not in prevID_array:
6040
                        if (!in_array($next_id, $prevId_array)) {
6041
                            $theList = array_merge(
6042
                                GeneralUtility::intExplode(
6043
                                    ',',
6044
                                    $this->getTreeList(
6045
                                        $next_id,
6046
                                        $depth - 1,
6047
                                        $begin - 1,
6048
                                        $dontCheckEnableFields,
6049
                                        $addSelectFields,
6050
                                        $moreWhereClauses,
6051
                                        $prevId_array,
6052
                                        $recursionLevel + 1
6053
                                    ),
6054
                                    true
6055
                                ),
6056
                                $theList
6057
                            );
6058
                        }
6059
                    }
6060
                }
6061
            }
6062
        }
6063
        // If first run, check if the ID should be returned:
6064
        if (!$recursionLevel) {
6065
            if ($addId) {
6066
                if ($begin > 0) {
6067
                    $theList[] = 0;
6068
                } else {
6069
                    $theList[] = $addId;
6070
                }
6071
            }
6072
6073
            $cacheEntry = [
6074
                'md5hash' => $requestHash,
6075
                'pid' => $id,
6076
                'treelist' => implode(',', $theList),
6077
                'tstamp' => $GLOBALS['EXEC_TIME'],
6078
            ];
6079
6080
            // Only add to cache if not logged into TYPO3 Backend
6081
            if (!$this->getFrontendBackendUser() instanceof AbstractUserAuthentication) {
0 ignored issues
show
introduced by
$this->getFrontendBackendUser() is always a sub-type of TYPO3\CMS\Core\Authentic...tractUserAuthentication.
Loading history...
6082
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
6083
                try {
6084
                    $connection->transactional(function ($connection) use ($cacheEntry) {
6085
                        $connection->insert('cache_treelist', $cacheEntry);
6086
                    });
6087
                } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
6088
                }
6089
            }
6090
        }
6091
6092
        return implode(',', $theList);
6093
    }
6094
6095
    /**
6096
     * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.)
6097
     * 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%")'
6098
     *
6099
     * @param string $searchWords The search words. These will be separated by space and comma.
6100
     * @param string $searchFieldList The fields to search in
6101
     * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well)
6102
     * @return string The WHERE clause.
6103
     */
6104
    public function searchWhere($searchWords, $searchFieldList, $searchTable)
6105
    {
6106
        if (!$searchWords) {
6107
            return '';
6108
        }
6109
6110
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
6111
            ->getQueryBuilderForTable($searchTable);
6112
6113
        $prefixTableName = $searchTable ? $searchTable . '.' : '';
6114
6115
        $where = $queryBuilder->expr()->andX();
6116
        $searchFields = explode(',', $searchFieldList);
6117
        $searchWords = preg_split('/[ ,]/', $searchWords);
6118
        foreach ($searchWords as $searchWord) {
6119
            $searchWord = trim($searchWord);
6120
            if (strlen($searchWord) < 3) {
6121
                continue;
6122
            }
6123
            $searchWordConstraint = $queryBuilder->expr()->orX();
6124
            $searchWord = $queryBuilder->escapeLikeWildcards($searchWord);
6125
            foreach ($searchFields as $field) {
6126
                $searchWordConstraint->add(
6127
                    $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%'))
6128
                );
6129
            }
6130
6131
            if ($searchWordConstraint->count()) {
6132
                $where->add($searchWordConstraint);
6133
            }
6134
        }
6135
6136
        if ((string)$where === '') {
6137
            return '';
6138
        }
6139
6140
        return ' AND (' . (string)$where . ')';
6141
    }
6142
6143
    /**
6144
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
6145
     * This function is preferred over ->getQuery() if you just need to create and then execute a query.
6146
     *
6147
     * @param string $table The table name
6148
     * @param array $conf The TypoScript configuration properties
6149
     * @return Statement
6150
     * @see getQuery()
6151
     */
6152
    public function exec_getQuery($table, $conf)
0 ignored issues
show
Coding Style introduced by
Method name "ContentObjectRenderer::exec_getQuery" is not in camel caps format
Loading history...
6153
    {
6154
        $statement = $this->getQuery($table, $conf);
6155
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6156
6157
        return $connection->executeQuery($statement);
6158
    }
6159
6160
    /**
6161
     * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array
6162
     * and overlays with translation and version if available
6163
     *
6164
     * @param string $tableName the name of the TCA database table
6165
     * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference
6166
     * @return array The records
6167
     * @throws \UnexpectedValueException
6168
     */
6169
    public function getRecords($tableName, array $queryConfiguration)
6170
    {
6171
        $records = [];
6172
6173
        $statement = $this->exec_getQuery($tableName, $queryConfiguration);
6174
6175
        $tsfe = $this->getTypoScriptFrontendController();
6176
        while ($row = $statement->fetch()) {
6177
            // Versioning preview:
6178
            $tsfe->sys_page->versionOL($tableName, $row, true);
6179
6180
            // Language overlay:
6181
            if (is_array($row)) {
6182
                $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row);
6183
            }
6184
6185
            // Might be unset in the language overlay
6186
            if (is_array($row)) {
6187
                $records[] = $row;
6188
            }
6189
        }
6190
6191
        return $records;
6192
    }
6193
6194
    /**
6195
     * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array
6196
     * Implements the "select" function in TypoScript
6197
     *
6198
     * @param string $table See ->exec_getQuery()
6199
     * @param array $conf See ->exec_getQuery()
6200
     * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED!
6201
     * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts.
6202
     * @throws \RuntimeException
6203
     * @throws \InvalidArgumentException
6204
     * @internal
6205
     * @see numRows()
6206
     */
6207
    public function getQuery($table, $conf, $returnQueryArray = false)
6208
    {
6209
        // Resolve stdWrap in these properties first
6210
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6211
        $properties = [
6212
            'pidInList',
6213
            'uidInList',
6214
            'languageField',
6215
            'selectFields',
6216
            'max',
6217
            'begin',
6218
            'groupBy',
6219
            'orderBy',
6220
            'join',
6221
            'leftjoin',
6222
            'rightjoin',
6223
            'recursive',
6224
            'where'
6225
        ];
6226
        foreach ($properties as $property) {
6227
            $conf[$property] = trim(
6228
                isset($conf[$property . '.'])
6229
                    ? $this->stdWrap($conf[$property], $conf[$property . '.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
6230
                    : $conf[$property]
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
6231
            );
6232
            if ($conf[$property] === '') {
6233
                unset($conf[$property]);
6234
            } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftJoin', 'rightJoin', 'where'], true)) {
6235
                $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]);
6236
            }
6237
            if (isset($conf[$property . '.'])) {
6238
                // stdWrapping already done, so remove the sub-array
6239
                unset($conf[$property . '.']);
6240
            }
6241
        }
6242
        // Handle PDO-style named parameter markers first
6243
        $queryMarkers = $this->getQueryMarkers($table, $conf);
6244
        // Replace the markers in the non-stdWrap properties
6245
        foreach ($queryMarkers as $marker => $markerValue) {
6246
            $properties = [
6247
                'uidInList',
6248
                'selectFields',
6249
                'where',
6250
                'max',
6251
                'begin',
6252
                'groupBy',
6253
                'orderBy',
6254
                'join',
6255
                'leftjoin',
6256
                'rightjoin'
6257
            ];
6258
            foreach ($properties as $property) {
6259
                if ($conf[$property]) {
6260
                    $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]);
6261
                }
6262
            }
6263
        }
6264
6265
        // Construct WHERE clause:
6266
        // Handle recursive function for the pidInList
6267
        if (isset($conf['recursive'])) {
6268
            $conf['recursive'] = (int)$conf['recursive'];
6269
            if ($conf['recursive'] > 0) {
6270
                $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true);
6271
                array_walk($pidList, function (&$storagePid) {
6272
                    if ($storagePid === 'this') {
6273
                        $storagePid = $this->getTypoScriptFrontendController()->id;
6274
                    }
6275
                    if ($storagePid > 0) {
6276
                        $storagePid = -$storagePid;
6277
                    }
6278
                });
6279
                $expandedPidList = [];
6280
                foreach ($pidList as $value) {
6281
                    // Implementation of getTreeList allows to pass the id negative to include
6282
                    // it into the result otherwise only childpages are returned
6283
                    $expandedPidList = array_merge(
6284
                        GeneralUtility::intExplode(',', $this->getTreeList($value, $conf['recursive'])),
6285
                        $expandedPidList
6286
                    );
6287
                }
6288
                $conf['pidInList'] = implode(',', $expandedPidList);
6289
            }
6290
        }
6291
        if ((string)$conf['pidInList'] === '') {
6292
            $conf['pidInList'] = 'this';
6293
        }
6294
6295
        $queryParts = $this->getQueryConstraints($table, $conf);
6296
6297
        $queryBuilder = $connection->createQueryBuilder();
6298
        // @todo Check against getQueryConstraints, can probably use FrontendRestrictions
6299
        // @todo here and remove enableFields there.
6300
        $queryBuilder->getRestrictions()->removeAll();
6301
        $queryBuilder->select('*')->from($table);
6302
6303
        if ($queryParts['where']) {
6304
            $queryBuilder->where($queryParts['where']);
6305
        }
6306
6307
        if ($queryParts['groupBy']) {
6308
            $queryBuilder->groupBy(...$queryParts['groupBy']);
6309
        }
6310
6311
        if (is_array($queryParts['orderBy'])) {
6312
            foreach ($queryParts['orderBy'] as $orderBy) {
6313
                $queryBuilder->addOrderBy(...$orderBy);
6314
            }
6315
        }
6316
6317
        // Fields:
6318
        if ($conf['selectFields']) {
6319
            $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table));
6320
        }
6321
6322
        // Setting LIMIT:
6323
        $error = false;
6324
        if ($conf['max'] || $conf['begin']) {
6325
            // Finding the total number of records, if used:
6326
            if (strpos(strtolower($conf['begin'] . $conf['max']), 'total') !== false) {
6327
                $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6328
                $countQueryBuilder->getRestrictions()->removeAll();
6329
                $countQueryBuilder->count('*')
6330
                    ->from($table)
6331
                    ->where($queryParts['where']);
6332
6333
                if ($queryParts['groupBy']) {
6334
                    $countQueryBuilder->groupBy(...$queryParts['groupBy']);
6335
                }
6336
6337
                try {
6338
                    $count = $countQueryBuilder->execute()->fetchColumn(0);
6339
                    $conf['max'] = str_ireplace('total', $count, $conf['max']);
6340
                    $conf['begin'] = str_ireplace('total', $count, $conf['begin']);
6341
                } catch (DBALException $e) {
6342
                    $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage());
6343
                    $error = true;
6344
                }
6345
            }
6346
6347
            if (!$error) {
6348
                $conf['begin'] = MathUtility::forceIntegerInRange(ceil($this->calc($conf['begin'])), 0);
0 ignored issues
show
Bug introduced by
ceil($this->calc($conf['begin'])) of type double is incompatible with the type integer expected by parameter $theInt of TYPO3\CMS\Core\Utility\M...::forceIntegerInRange(). ( Ignorable by Annotation )

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

6348
                $conf['begin'] = MathUtility::forceIntegerInRange(/** @scrutinizer ignore-type */ ceil($this->calc($conf['begin'])), 0);
Loading history...
6349
                $conf['max'] = MathUtility::forceIntegerInRange(ceil($this->calc($conf['max'])), 0);
6350
                if ($conf['begin'] > 0) {
6351
                    $queryBuilder->setFirstResult($conf['begin']);
6352
                }
6353
                $queryBuilder->setMaxResults($conf['max'] ?: 100000);
6354
            }
6355
        }
6356
6357
        if (!$error) {
6358
            // Setting up tablejoins:
6359
            if ($conf['join']) {
6360
                $joinParts = QueryHelper::parseJoin($conf['join']);
6361
                $queryBuilder->join(
6362
                    $table,
6363
                    $joinParts['tableName'],
6364
                    $joinParts['tableAlias'],
6365
                    $joinParts['joinCondition']
6366
                );
6367
            } elseif ($conf['leftjoin']) {
6368
                $joinParts = QueryHelper::parseJoin($conf['leftjoin']);
6369
                $queryBuilder->leftJoin(
6370
                    $table,
6371
                    $joinParts['tableName'],
6372
                    $joinParts['tableAlias'],
6373
                    $joinParts['joinCondition']
6374
                );
6375
            } elseif ($conf['rightjoin']) {
6376
                $joinParts = QueryHelper::parseJoin($conf['rightjoin']);
6377
                $queryBuilder->rightJoin(
6378
                    $table,
6379
                    $joinParts['tableName'],
6380
                    $joinParts['tableAlias'],
6381
                    $joinParts['joinCondition']
6382
                );
6383
            }
6384
6385
            // Convert the QueryBuilder object into a SQL statement.
6386
            $query = $queryBuilder->getSQL();
6387
6388
            // Replace the markers in the queryParts to handle stdWrap enabled properties
6389
            foreach ($queryMarkers as $marker => $markerValue) {
6390
                // @todo Ugly hack that needs to be cleaned up, with the current architecture
6391
                // @todo for exec_Query / getQuery it's the best we can do.
6392
                $query = str_replace('###' . $marker . '###', $markerValue, $query);
6393
                foreach ($queryParts as $queryPartKey => &$queryPartValue) {
6394
                    $queryPartValue = str_replace('###' . $marker . '###', $markerValue, $queryPartValue);
6395
                }
6396
                unset($queryPartValue);
6397
            }
6398
6399
            return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
6400
        }
6401
6402
        return '';
6403
    }
6404
6405
    /**
6406
     * Helper to transform a QueryBuilder object into a queryParts array that can be used
6407
     * with exec_SELECT_queryArray
6408
     *
6409
     * @param \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder
6410
     * @return array
6411
     * @throws \RuntimeException
6412
     */
6413
    protected function getQueryArray(QueryBuilder $queryBuilder)
6414
    {
6415
        $fromClauses = [];
6416
        $knownAliases = [];
6417
        $queryParts = [];
6418
6419
        // Loop through all FROM clauses
6420
        foreach ($queryBuilder->getQueryPart('from') as $from) {
6421
            if ($from['alias'] === null) {
6422
                $tableSql = $from['table'];
6423
                $tableReference = $from['table'];
6424
            } else {
6425
                $tableSql = $from['table'] . ' ' . $from['alias'];
6426
                $tableReference = $from['alias'];
6427
            }
6428
6429
            $knownAliases[$tableReference] = true;
6430
6431
            $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
6432
                $tableReference,
6433
                $queryBuilder->getQueryPart('join'),
6434
                $knownAliases
6435
            );
6436
        }
6437
6438
        $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
6439
        $queryParts['FROM'] = implode(', ', $fromClauses);
6440
        $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
6441
        $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
6442
        $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
6443
        if ($queryBuilder->getFirstResult() > 0) {
6444
            $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
6445
        } elseif ($queryBuilder->getMaxResults() > 0) {
6446
            $queryParts['LIMIT'] = $queryBuilder->getMaxResults();
6447
        }
6448
6449
        return $queryParts;
6450
    }
6451
6452
    /**
6453
     * Helper to transform the QueryBuilder join part into a SQL fragment.
6454
     *
6455
     * @param string $fromAlias
6456
     * @param array $joinParts
6457
     * @param array $knownAliases
6458
     * @return string
6459
     * @throws \RuntimeException
6460
     */
6461
    protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
6462
    {
6463
        $sql = '';
6464
6465
        if (isset($joinParts['join'][$fromAlias])) {
6466
            foreach ($joinParts['join'][$fromAlias] as $join) {
6467
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
6468
                    throw new \RuntimeException(
6469
                        'Non unique join alias: "' . $join['joinAlias'] . '" found.',
6470
                        1472748872
6471
                    );
6472
                }
6473
                $sql .= ' ' . strtoupper($join['joinType'])
6474
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
6475
                    . ' ON ' . ((string)$join['joinCondition']);
6476
                $knownAliases[$join['joinAlias']] = true;
6477
            }
6478
6479
            foreach ($joinParts['join'][$fromAlias] as $join) {
6480
                $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
6481
            }
6482
        }
6483
6484
        return $sql;
6485
    }
6486
    /**
6487
     * Helper function for getQuery(), creating the WHERE clause of the SELECT query
6488
     *
6489
     * @param string $table The table name
6490
     * @param array $conf The TypoScript configuration properties
6491
     * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments
6492
     * @throws \InvalidArgumentException
6493
     * @see getQuery()
6494
     */
6495
    protected function getQueryConstraints(string $table, array $conf): array
6496
    {
6497
        // Init:
6498
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6499
        $expressionBuilder = $queryBuilder->expr();
6500
        $tsfe = $this->getTypoScriptFrontendController();
6501
        $constraints = [];
6502
        $pid_uid_flag = 0;
6503
        $enableFieldsIgnore = [];
6504
        $queryParts = [
6505
            'where' => null,
6506
            'groupBy' => null,
6507
            'orderBy' => null,
6508
        ];
6509
6510
        $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
6511
        $considerMovePlaceholders = (
6512
            $isInWorkspace && $table !== 'pages'
6513
            && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
6514
        );
6515
6516
        if (trim($conf['uidInList'])) {
6517
            $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $conf['uidInList']));
6518
6519
            // If move placeholder shall be considered, select via t3ver_move_id
6520
            if ($considerMovePlaceholders) {
6521
                $constraints[] = (string)$expressionBuilder->orX(
6522
                    $expressionBuilder->in($table . '.uid', $listArr),
6523
                    $expressionBuilder->andX(
6524
                        $expressionBuilder->eq(
6525
                            $table . '.t3ver_state',
6526
                            (int)(string)VersionState::cast(VersionState::MOVE_PLACEHOLDER)
6527
                        ),
6528
                        $expressionBuilder->in($table . '.t3ver_move_id', $listArr)
6529
                    )
6530
                );
6531
            } else {
6532
                $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr);
6533
            }
6534
            $pid_uid_flag++;
6535
        }
6536
6537
        // Static_* tables are allowed to be fetched from root page
6538
        if (strpos($table, 'static_') === 0) {
6539
            $pid_uid_flag++;
6540
        }
6541
6542
        if (trim($conf['pidInList'])) {
6543
            $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $conf['pidInList']));
6544
            // Removes all pages which are not visible for the user!
6545
            $listArr = $this->checkPidArray($listArr);
6546
            if (GeneralUtility::inList($conf['pidInList'], 'root')) {
6547
                $listArr[] = 0;
6548
            }
6549
            if (GeneralUtility::inList($conf['pidInList'], '-1')) {
6550
                $listArr[] = -1;
6551
                $enableFieldsIgnore['pid'] = true;
6552
            }
6553
            if (!empty($listArr)) {
6554
                $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr));
6555
                $pid_uid_flag++;
6556
            } else {
6557
                // If not uid and not pid then uid is set to 0 - which results in nothing!!
6558
                $pid_uid_flag = 0;
6559
            }
6560
        }
6561
6562
        // If not uid and not pid then uid is set to 0 - which results in nothing!!
6563
        if (!$pid_uid_flag) {
6564
            $constraints[] = $expressionBuilder->eq($table . '.uid', 0);
6565
        }
6566
6567
        $where = isset($conf['where.']) ? trim($this->stdWrap($conf['where'], $conf['where.'])) : trim($conf['where']);
6568
        if ($where) {
6569
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
6570
        }
6571
6572
        // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched
6573
        // but only do this for TCA tables that have languages enabled
6574
        $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class));
6575
        if ($languageConstraint !== null) {
6576
            $constraints[] = $languageConstraint;
6577
        }
6578
6579
        // Enablefields
6580
        if ($table === 'pages') {
6581
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
6582
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
6583
        } else {
6584
            $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
6585
        }
6586
6587
        // MAKE WHERE:
6588
        if (count($constraints) !== 0) {
6589
            $queryParts['where'] = $expressionBuilder->andX(...$constraints);
6590
        }
6591
        // GROUP BY
6592
        if (trim($conf['groupBy'])) {
6593
            $groupBy = isset($conf['groupBy.'])
6594
                ? trim($this->stdWrap($conf['groupBy'], $conf['groupBy.']))
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
6595
                : trim($conf['groupBy']);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
6596
            $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy);
6597
        }
6598
6599
        // ORDER BY
6600
        if (trim($conf['orderBy'])) {
6601
            $orderByString = isset($conf['orderBy.'])
6602
                ? trim($this->stdWrap($conf['orderBy'], $conf['orderBy.']))
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
6603
                : trim($conf['orderBy']);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
6604
6605
            $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString);
6606
        }
6607
6608
        // Return result:
6609
        return $queryParts;
6610
    }
6611
6612
    /**
6613
     * Adds parts to the WHERE clause that are related to language.
6614
     * This only works on TCA tables which have the [ctrl][languageField] field set or if they
6615
     * have select.languageField = my_language_field set explicitly.
6616
     *
6617
     * It is also possible to disable the language restriction for a query by using select.languageField = 0,
6618
     * if select.languageField is not explicitly set, the TCA default values are taken.
6619
     *
6620
     * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted:
6621
     *
6622
     * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are
6623
     * fetched (the overlays are taken care of later-on).
6624
     * if the current language has overlays but also records without localization-parent (free mode) available,
6625
     * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1
6626
     * which overrules the overlayType within the language aspect.
6627
     *
6628
     * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records
6629
     * for the current language.
6630
     *
6631
     * @param ExpressionBuilder $expressionBuilder
6632
     * @param string $table
6633
     * @param array $conf
6634
     * @param Context $context
6635
     * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null
6636
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
6637
     */
6638
    protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
6639
    {
6640
        $languageField = '';
6641
        $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
6642
        // Check if the table is translatable, and set the language field by default from the TCA information
6643
        if (!empty($conf['languageField']) || !isset($conf['languageField'])) {
6644
            if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) {
6645
                $languageField = $conf['languageField'];
6646
            } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) {
6647
                $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
6648
            }
6649
        }
6650
6651
        // No language restriction enabled explicitly or available via TCA
6652
        if (empty($languageField)) {
6653
            return null;
6654
        }
6655
6656
        /** @var LanguageAspect $languageAspect */
6657
        $languageAspect = $context->getAspect('language');
6658
        if ($languageAspect->doOverlays() && !empty($localizationParentField)) {
6659
            // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will
6660
            // OVERLAY the records with localized versions!
6661
            $languageQuery = $expressionBuilder->in($languageField, [0, -1]);
6662
            // Use this option to include records that don't have a default language counterpart ("free mode")
6663
            // (originalpointerfield is 0 and the language field contains the requested language)
6664
            if (isset($conf['includeRecordsWithoutDefaultTranslation']) || $conf['includeRecordsWithoutDefaultTranslation.']) {
6665
                $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ?
6666
                    $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation'];
6667
                $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== '';
6668
            } else {
6669
                // Option was not explicitly set, check what's in for the language overlay type.
6670
                $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING;
6671
            }
6672
            if ($includeRecordsWithoutDefaultTranslation) {
6673
                $languageQuery = $expressionBuilder->orX(
6674
                    $languageQuery,
6675
                    $expressionBuilder->andX(
6676
                        $expressionBuilder->eq($table . '.' . $localizationParentField, 0),
6677
                        $expressionBuilder->eq($languageField, $languageAspect->getContentId())
6678
                    )
6679
                );
6680
            }
6681
            return $languageQuery;
6682
        }
6683
        // No overlays = only fetch records given for the requested language and "all languages"
6684
        return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]);
6685
    }
6686
6687
    /**
6688
     * Helper function for getQuery, sanitizing the select part
6689
     *
6690
     * This functions checks if the necessary fields are part of the select
6691
     * and adds them if necessary.
6692
     *
6693
     * @param string $selectPart Select part
6694
     * @param string $table Table to select from
6695
     * @return string Sanitized select part
6696
     * @internal
6697
     * @see getQuery
6698
     */
6699
    protected function sanitizeSelectPart($selectPart, $table)
6700
    {
6701
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6702
6703
        // Pattern matching parts
6704
        $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
6705
        $matchEnd = '(\\s*,|\\s*$)/';
6706
        $necessaryFields = ['uid', 'pid'];
6707
        $wsFields = ['t3ver_state'];
6708
        if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) {
6709
            foreach ($necessaryFields as $field) {
6710
                $match = $matchStart . $field . $matchEnd;
6711
                if (!preg_match($match, $selectPart)) {
6712
                    $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6713
                }
6714
            }
6715
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
6716
                foreach ($wsFields as $field) {
6717
                    $match = $matchStart . $field . $matchEnd;
6718
                    if (!preg_match($match, $selectPart)) {
6719
                        $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6720
                    }
6721
                }
6722
            }
6723
        }
6724
        return $selectPart;
6725
    }
6726
6727
    /**
6728
     * 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)
6729
     *
6730
     * @param array $listArr Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed.
6731
     * @return array Returns the array of remaining page UID numbers
6732
     * @internal
6733
     * @see checkPid()
6734
     */
6735
    public function checkPidArray($listArr)
6736
    {
6737
        if (!is_array($listArr) || empty($listArr)) {
0 ignored issues
show
introduced by
The condition is_array($listArr) is always true.
Loading history...
6738
            return [];
6739
        }
6740
        $outArr = [];
6741
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6742
        $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
6743
        $queryBuilder->select('uid')
6744
            ->from('pages')
6745
            ->where(
6746
                $queryBuilder->expr()->in(
6747
                    'uid',
6748
                    $queryBuilder->createNamedParameter($listArr, Connection::PARAM_INT_ARRAY)
6749
                ),
6750
                $queryBuilder->expr()->notIn(
6751
                    'doktype',
6752
                    $queryBuilder->createNamedParameter(
6753
                        GeneralUtility::intExplode(',', $this->checkPid_badDoktypeList, true),
6754
                        Connection::PARAM_INT_ARRAY
6755
                    )
6756
                )
6757
            );
6758
        try {
6759
            $result = $queryBuilder->execute();
6760
            while ($row = $result->fetch()) {
6761
                $outArr[] = $row['uid'];
6762
            }
6763
        } catch (DBALException $e) {
6764
            $this->getTimeTracker()->setTSlogMessage($e->getMessage() . ': ' . $queryBuilder->getSQL(), 3);
6765
        }
6766
6767
        return $outArr;
6768
    }
6769
6770
    /**
6771
     * Checks if a page UID is available due to enableFields() AND the list of bad doktype numbers ($this->checkPid_badDoktypeList)
6772
     *
6773
     * @param int $uid Page UID to test
6774
     * @return bool TRUE if OK
6775
     * @internal
6776
     * @see checkPidArray()
6777
     */
6778
    public function checkPid($uid)
6779
    {
6780
        $uid = (int)$uid;
6781
        if (!isset($this->checkPid_cache[$uid])) {
6782
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6783
            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
6784
            $count = $queryBuilder->count('*')
6785
                ->from('pages')
6786
                ->where(
6787
                    $queryBuilder->expr()->eq(
6788
                        'uid',
6789
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
6790
                    ),
6791
                    $queryBuilder->expr()->notIn(
6792
                        'doktype',
6793
                        $queryBuilder->createNamedParameter(
6794
                            GeneralUtility::intExplode(',', $this->checkPid_badDoktypeList, true),
6795
                            Connection::PARAM_INT_ARRAY
6796
                        )
6797
                    )
6798
                )
6799
                ->execute()
6800
                ->fetchColumn(0);
6801
6802
            $this->checkPid_cache[$uid] = (bool)$count;
6803
        }
6804
        return $this->checkPid_cache[$uid];
6805
    }
6806
6807
    /**
6808
     * Builds list of marker values for handling PDO-like parameter markers in select parts.
6809
     * 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.
6810
     *
6811
     * @param string $table Table to select records from
6812
     * @param array $conf Select part of CONTENT definition
6813
     * @return array List of values to replace markers with
6814
     * @internal
6815
     * @see getQuery()
6816
     */
6817
    public function getQueryMarkers($table, $conf)
6818
    {
6819
        if (!is_array($conf['markers.'])) {
6820
            return [];
6821
        }
6822
        // Parse markers and prepare their values
6823
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6824
        $markerValues = [];
6825
        foreach ($conf['markers.'] as $dottedMarker => $dummy) {
6826
            $marker = rtrim($dottedMarker, '.');
6827
            if ($dottedMarker != $marker . '.') {
6828
                continue;
6829
            }
6830
            // Parse definition
6831
            $tempValue = isset($conf['markers.'][$dottedMarker])
6832
                ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'], $conf['markers.'][$dottedMarker])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
6833
                : $conf['markers.'][$dottedMarker]['value'];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
6834
            // Quote/escape if needed
6835
            if (is_numeric($tempValue)) {
6836
                if ((int)$tempValue == $tempValue) {
6837
                    // Handle integer
6838
                    $markerValues[$marker] = (int)$tempValue;
6839
                } else {
6840
                    // Handle float
6841
                    $markerValues[$marker] = (float)$tempValue;
6842
                }
6843
            } elseif ($tempValue === null) {
6844
                // It represents NULL
6845
                $markerValues[$marker] = 'NULL';
6846
            } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
6847
                // See if it is really a comma separated list of values
6848
                $explodeValues = GeneralUtility::trimExplode(',', $tempValue);
6849
                if (count($explodeValues) > 1) {
6850
                    // Handle each element of list separately
6851
                    $tempArray = [];
6852
                    foreach ($explodeValues as $listValue) {
6853
                        if (is_numeric($listValue)) {
6854
                            if ((int)$listValue == $listValue) {
6855
                                $tempArray[] = (int)$listValue;
6856
                            } else {
6857
                                $tempArray[] = (float)$listValue;
6858
                            }
6859
                        } else {
6860
                            // If quoted, remove quotes before
6861
                            // escaping.
6862
                            if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
6863
                                $listValue = $matches[1];
6864
                            } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
6865
                                $listValue = $matches[1];
6866
                            }
6867
                            $tempArray[] = $connection->quote($listValue);
6868
                        }
6869
                    }
6870
                    $markerValues[$marker] = implode(',', $tempArray);
6871
                } else {
6872
                    // Handle remaining values as string
6873
                    $markerValues[$marker] = $connection->quote($tempValue);
6874
                }
6875
            } else {
6876
                // Handle remaining values as string
6877
                $markerValues[$marker] = $connection->quote($tempValue);
6878
            }
6879
        }
6880
        return $markerValues;
6881
    }
6882
6883
    /***********************************************
6884
     *
6885
     * Frontend editing functions
6886
     *
6887
     ***********************************************/
6888
    /**
6889
     * 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.
6890
     * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
6891
     * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
6892
     *
6893
     * @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.
6894
     * @param array $conf TypoScript configuration properties for the editPanel
6895
     * @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
6896
     * @param array $dataArray Alternative data array to use. Default is $this->data
6897
     * @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.
6898
     */
6899
    public function editPanel($content, $conf, $currentRecord = '', $dataArray = [])
6900
    {
6901
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6902
            return $content;
6903
        }
6904
        if (!$this->getTypoScriptFrontendController()->displayEditIcons) {
6905
            return $content;
6906
        }
6907
6908
        if (!$currentRecord) {
6909
            $currentRecord = $this->currentRecord;
6910
        }
6911
        if (empty($dataArray)) {
6912
            $dataArray = $this->data;
6913
        }
6914
6915
        if ($conf['newRecordFromTable']) {
6916
            $currentRecord = $conf['newRecordFromTable'] . ':NEW';
6917
            $conf['allow'] = 'new';
6918
            $checkEditAccessInternals = false;
6919
        } else {
6920
            $checkEditAccessInternals = true;
6921
        }
6922
        [$table, $uid] = explode(':', $currentRecord);
6923
        // Page ID for new records, 0 if not specified
6924
        $newRecordPid = (int)$conf['newRecordInPid'];
6925
        $newUid = null;
6926
        if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) {
6927
            if ($table === 'pages') {
6928
                $newUid = $uid;
6929
            } else {
6930
                if ($conf['newRecordFromTable']) {
6931
                    $newUid = $this->getTypoScriptFrontendController()->id;
6932
                    if ($newRecordPid) {
6933
                        $newUid = $newRecordPid;
6934
                    }
6935
                } else {
6936
                    $newUid = -1 * $uid;
6937
                }
6938
            }
6939
        }
6940
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6941
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6942
            if ($editClass) {
6943
                $edit = GeneralUtility::makeInstance($editClass);
6944
                $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']);
6945
                $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []);
6946
            }
6947
        }
6948
        return $content;
6949
    }
6950
6951
    /**
6952
     * 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.
6953
     * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
6954
     *
6955
     * @param string $content The content to which the edit icons should be appended
6956
     * @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
6957
     * @param array $conf TypoScript properties for configuring the edit icons.
6958
     * @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
6959
     * @param array $dataArray Alternative data array to use. Default is $this->data
6960
     * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine
6961
     * @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.
6962
     */
6963
    public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '')
6964
    {
6965
        if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6966
            return $content;
6967
        }
6968
        if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) {
6969
            return $content;
6970
        }
6971
        if (!$currentRecord) {
6972
            $currentRecord = $this->currentRecord;
6973
        }
6974
        if (empty($dataArray)) {
6975
            $dataArray = $this->data;
6976
        }
6977
        // Check incoming params:
6978
        [$currentRecordTable, $currentRecordUID] = explode(':', $currentRecord);
6979
        [$fieldList, $table] = array_reverse(GeneralUtility::trimExplode(':', $params, true));
6980
        // Reverse the array because table is optional
6981
        if (!$table) {
6982
            $table = $currentRecordTable;
6983
        } elseif ($table != $currentRecordTable) {
6984
            // If the table is set as the first parameter, and does not match the table of the current record, then just return.
6985
            return $content;
6986
        }
6987
6988
        $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID;
6989
        // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
6990
        if (!array_key_exists('allow', $conf)) {
6991
            $conf['allow'] = 'edit';
6992
        }
6993
        if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6994
            $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6995
            if ($editClass) {
6996
                $edit = GeneralUtility::makeInstance($editClass);
6997
                $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
6998
            }
6999
        }
7000
        return $content;
7001
    }
7002
7003
    /**
7004
     * Returns TRUE if the input table/row would be hidden in the frontend (according nto the current time and simulate user group)
7005
     *
7006
     * @param string $table The table name
7007
     * @param array $row The data record
7008
     * @return bool
7009
     * @internal
7010
     * @see editPanelPreviewBorder()
7011
     */
7012
    public function isDisabled($table, $row)
7013
    {
7014
        $tsfe = $this->getTypoScriptFrontendController();
7015
        $enablecolumns = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
7016
        return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']]
7017
            || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup
7018
            || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > $GLOBALS['EXEC_TIME']
7019
            || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < $GLOBALS['EXEC_TIME'];
7020
    }
7021
7022
    /**
7023
     * Get instance of FAL resource factory
7024
     *
7025
     * @return ResourceFactory
7026
     */
7027
    protected function getResourceFactory()
7028
    {
7029
        return GeneralUtility::makeInstance(ResourceFactory::class);
7030
    }
7031
7032
    /**
7033
     * Wrapper function for GeneralUtility::getIndpEnv()
7034
     *
7035
     * @see GeneralUtility::getIndpEnv
7036
     * @param string $key Name of the "environment variable"/"server variable" you wish to get.
7037
     * @return string
7038
     */
7039
    protected function getEnvironmentVariable($key)
7040
    {
7041
        return GeneralUtility::getIndpEnv($key);
7042
    }
7043
7044
    /**
7045
     * Fetches content from cache
7046
     *
7047
     * @param array $configuration Array
7048
     * @return string|bool FALSE on cache miss
7049
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
7050
     */
7051
    protected function getFromCache(array $configuration)
7052
    {
7053
        $content = false;
7054
7055
        if ($this->getTypoScriptFrontendController()->no_cache) {
7056
            return $content;
7057
        }
7058
        $cacheKey = $this->calculateCacheKey($configuration);
7059
        if (!empty($cacheKey)) {
7060
            /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */
7061
            $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)
7062
                ->getCache('hash');
7063
            $content = $cacheFrontend->get($cacheKey);
7064
        }
7065
        return $content;
7066
    }
7067
7068
    /**
7069
     * Calculates the lifetime of a cache entry based on the given configuration
7070
     *
7071
     * @param array $configuration
7072
     * @return int|null
7073
     */
7074
    protected function calculateCacheLifetime(array $configuration)
7075
    {
7076
        $lifetimeConfiguration = $configuration['lifetime'] ?? '';
7077
        $lifetimeConfiguration = isset($configuration['lifetime.'])
7078
            ? $this->stdWrap($lifetimeConfiguration, $configuration['lifetime.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
7079
            : $lifetimeConfiguration;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
7080
7081
        $lifetime = null; // default lifetime
7082
        if (strtolower($lifetimeConfiguration) === 'unlimited') {
7083
            $lifetime = 0; // unlimited
7084
        } elseif ($lifetimeConfiguration > 0) {
7085
            $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds
7086
        }
7087
        return $lifetime;
7088
    }
7089
7090
    /**
7091
     * Calculates the tags for a cache entry bases on the given configuration
7092
     *
7093
     * @param array $configuration
7094
     * @return array
7095
     */
7096
    protected function calculateCacheTags(array $configuration)
7097
    {
7098
        $tags = $configuration['tags'] ?? '';
7099
        $tags = isset($configuration['tags.'])
7100
            ? $this->stdWrap($tags, $configuration['tags.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
7101
            : $tags;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
7102
        return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags);
7103
    }
7104
7105
    /**
7106
     * Applies stdWrap to the cache key
7107
     *
7108
     * @param array $configuration
7109
     * @return string
7110
     */
7111
    protected function calculateCacheKey(array $configuration)
7112
    {
7113
        $key = $configuration['key'] ?? '';
7114
        return isset($configuration['key.'])
7115
            ? $this->stdWrap($key, $configuration['key.'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
7116
            : $key;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
7117
    }
7118
7119
    /**
7120
     * Returns the current BE user.
7121
     *
7122
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
7123
     */
7124
    protected function getFrontendBackendUser()
7125
    {
7126
        return $GLOBALS['BE_USER'];
7127
    }
7128
7129
    /**
7130
     * @return TimeTracker
7131
     */
7132
    protected function getTimeTracker()
7133
    {
7134
        return GeneralUtility::makeInstance(TimeTracker::class);
7135
    }
7136
7137
    /**
7138
     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
7139
     */
7140
    protected function getTypoScriptFrontendController()
7141
    {
7142
        return $this->typoScriptFrontendController ?: $GLOBALS['TSFE'];
7143
    }
7144
7145
    /**
7146
     * Support anchors without href value
7147
     * Changes ContentObjectRenderer::typolink to render a tag without href,
7148
     * if id or name attribute is present.
7149
     *
7150
     * @param string $linkText
7151
     * @param array $conf Typolink configuration decoded as array
7152
     * @return string Full a-Tag or just the linktext if id or name are not set.
7153
     */
7154
    protected function resolveAnchorLink(string $linkText, array $conf): string
7155
    {
7156
        $anchorTag = '<a ' . $this->getATagParams($conf) . '>';
7157
        $aTagParams = GeneralUtility::get_tag_attributes($anchorTag);
7158
        // If it looks like a anchor tag, render it anyway
7159
        if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
7160
            return $anchorTag . $linkText . '</a>';
7161
        }
7162
        // Otherwise just return the link text
7163
        return $linkText;
7164
    }
7165
7166
    /**
7167
     * Get content length of the current tag that could also contain nested tag contents
7168
     *
7169
     * @param string $theValue
7170
     * @param int $pointer
7171
     * @param string $currentTag
7172
     * @return int
7173
     */
7174
    protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int
7175
    {
7176
        $tempContent = strtolower(substr($theValue, $pointer));
7177
        $startTag = '<' . $currentTag;
7178
        $endTag = '</' . $currentTag . '>';
7179
        $offsetCount = 0;
7180
7181
        // Take care for nested tags
7182
        do {
7183
            $nextMatchingEndTagPosition = strpos($tempContent, $endTag);
7184
            $nextSameTypeTagPosition = strpos($tempContent, $startTag);
7185
7186
            // filter out nested tag contents to help getting the correct closing tag
7187
            if ($nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) {
7188
                $lastOpeningTagStartPosition = strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag);
7189
                $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag);
7190
                $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition;
7191
7192
                // replace content from latest tag start to latest tag end
7193
                $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition);
7194
            }
7195
        } while (
7196
            ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) &&
7197
            $nextSameTypeTagPosition < $nextMatchingEndTagPosition
7198
        );
7199
7200
        // if no closing tag is found we use length of the whole content
7201
        $endingOffset = strlen($tempContent);
7202
        if ($nextMatchingEndTagPosition !== false) {
7203
            $endingOffset = $nextMatchingEndTagPosition + $offsetCount;
7204
        }
7205
7206
        return $endingOffset;
7207
    }
7208
}
7209