BackendUtility::getLiveVersionIdOfRecord()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 18
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 2
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Utility;
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use Psr\Log\LoggerInterface;
20
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
21
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
22
use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
23
use TYPO3\CMS\Backend\Routing\Route;
24
use TYPO3\CMS\Backend\Routing\UriBuilder;
25
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26
use TYPO3\CMS\Core\Cache\CacheManager;
27
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
28
use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
29
use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
30
use TYPO3\CMS\Core\Context\Context;
31
use TYPO3\CMS\Core\Context\DateTimeAspect;
32
use TYPO3\CMS\Core\Core\Environment;
33
use TYPO3\CMS\Core\Database\Connection;
34
use TYPO3\CMS\Core\Database\ConnectionPool;
35
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
36
use TYPO3\CMS\Core\Database\Query\QueryHelper;
37
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
38
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
39
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
40
use TYPO3\CMS\Core\Database\RelationHandler;
41
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
42
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
43
use TYPO3\CMS\Core\Imaging\Icon;
44
use TYPO3\CMS\Core\Imaging\IconFactory;
45
use TYPO3\CMS\Core\Imaging\ImageDimension;
46
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
47
use TYPO3\CMS\Core\Localization\LanguageService;
48
use TYPO3\CMS\Core\Log\LogManager;
49
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
50
use TYPO3\CMS\Core\Resource\ProcessedFile;
51
use TYPO3\CMS\Core\Resource\ResourceFactory;
52
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
53
use TYPO3\CMS\Core\Routing\RouterInterface;
54
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
55
use TYPO3\CMS\Core\Site\SiteFinder;
56
use TYPO3\CMS\Core\Type\Bitmask\Permission;
57
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
58
use TYPO3\CMS\Core\Utility\ArrayUtility;
59
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
60
use TYPO3\CMS\Core\Utility\GeneralUtility;
61
use TYPO3\CMS\Core\Utility\HttpUtility;
62
use TYPO3\CMS\Core\Utility\MathUtility;
63
use TYPO3\CMS\Core\Utility\PathUtility;
64
use TYPO3\CMS\Core\Utility\StringUtility;
65
use TYPO3\CMS\Core\Versioning\VersionState;
66
67
/**
68
 * Standard functions available for the TYPO3 backend.
69
 * You are encouraged to use this class in your own applications (Backend Modules)
70
 * Don't instantiate - call functions with "\TYPO3\CMS\Backend\Utility\BackendUtility::" prefixed the function name.
71
 *
72
 * Call ALL methods without making an object!
73
 * Eg. to get a page-record 51 do this: '\TYPO3\CMS\Backend\Utility\BackendUtility::getRecord('pages',51)'
74
 */
75
class BackendUtility
76
{
77
    /*******************************************
78
     *
79
     * SQL-related, selecting records, searching
80
     *
81
     *******************************************/
82
    /**
83
     * Gets record with uid = $uid from $table
84
     * You can set $field to a list of fields (default is '*')
85
     * Additional WHERE clauses can be added by $where (fx. ' AND some_field = 1')
86
     * Will automatically check if records has been deleted and if so, not return anything.
87
     * $table must be found in $GLOBALS['TCA']
88
     *
89
     * @param string $table Table name present in $GLOBALS['TCA']
90
     * @param int|string $uid UID of record
91
     * @param string $fields List of fields to select
92
     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
93
     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
94
     * @return array|null Returns the row if found, otherwise NULL
95
     */
96
    public static function getRecord($table, $uid, $fields = '*', $where = '', $useDeleteClause = true)
97
    {
98
        // Ensure we have a valid uid (not 0 and not NEWxxxx) and a valid TCA
99
        if ((int)$uid && !empty($GLOBALS['TCA'][$table])) {
100
            $queryBuilder = static::getQueryBuilderForTable($table);
101
102
            // do not use enabled fields here
103
            $queryBuilder->getRestrictions()->removeAll();
104
105
            // should the delete clause be used
106
            if ($useDeleteClause) {
107
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
108
            }
109
110
            // set table and where clause
111
            $queryBuilder
112
                ->select(...GeneralUtility::trimExplode(',', $fields, true))
113
                ->from($table)
114
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)));
115
116
            // add custom where clause
117
            if ($where) {
118
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
119
            }
120
121
            $row = $queryBuilder->execute()->fetchAssociative();
122
            if ($row) {
123
                return $row;
124
            }
125
        }
126
        return null;
127
    }
128
129
    /**
130
     * Like getRecord(), but overlays workspace version if any.
131
     *
132
     * @param string $table Table name present in $GLOBALS['TCA']
133
     * @param int $uid UID of record
134
     * @param string $fields List of fields to select
135
     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
136
     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
137
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
138
     * @return array Returns the row if found, otherwise nothing
139
     */
140
    public static function getRecordWSOL(
141
        $table,
142
        $uid,
143
        $fields = '*',
144
        $where = '',
145
        $useDeleteClause = true,
146
        $unsetMovePointers = false
147
    ) {
148
        if ($fields !== '*') {
149
            $internalFields = StringUtility::uniqueList($fields . ',uid,pid');
150
            $row = self::getRecord($table, $uid, $internalFields, $where, $useDeleteClause);
151
            self::workspaceOL($table, $row, -99, $unsetMovePointers);
152
            if (is_array($row)) {
153
                foreach ($row as $key => $_) {
154
                    if (!GeneralUtility::inList($fields, $key) && $key[0] !== '_') {
155
                        unset($row[$key]);
156
                    }
157
                }
158
            }
159
        } else {
160
            $row = self::getRecord($table, $uid, $fields, $where, $useDeleteClause);
161
            self::workspaceOL($table, $row, -99, $unsetMovePointers);
162
        }
163
        return $row;
164
    }
165
166
    /**
167
     * Purges computed properties starting with underscore character ('_').
168
     *
169
     * @param array<string,mixed> $record
170
     * @return array<string,mixed>
171
     * @internal should only be used from within TYPO3 Core
172
     */
173
    public static function purgeComputedPropertiesFromRecord(array $record): array
174
    {
175
        return array_filter(
176
            $record,
177
            static function (string $propertyName): bool {
178
                return $propertyName[0] !== '_';
179
            },
180
            ARRAY_FILTER_USE_KEY
181
        );
182
    }
183
184
    /**
185
     * Purges computed property names starting with underscore character ('_').
186
     *
187
     * @param array $propertyNames
188
     * @return array
189
     * @internal should only be used from within TYPO3 Core
190
     */
191
    public static function purgeComputedPropertyNames(array $propertyNames): array
192
    {
193
        return array_filter(
194
            $propertyNames,
195
            static function (string $propertyName): bool {
196
                return $propertyName[0] !== '_';
197
            }
198
        );
199
    }
200
201
    /**
202
     * Makes a backwards explode on the $str and returns an array with ($table, $uid).
203
     * Example: tt_content_45 => ['tt_content', 45]
204
     *
205
     * @param string $str [tablename]_[uid] string to explode
206
     * @return array
207
     * @internal should only be used from within TYPO3 Core
208
     */
209
    public static function splitTable_Uid($str)
210
    {
211
        $split = explode('_', strrev($str), 2);
212
        $uid = $split[0];
213
        $table = $split[1] ?? '';
214
        return [strrev($table), strrev($uid)];
215
    }
216
217
    /**
218
     * Backend implementation of enableFields()
219
     * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime.
220
     * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition.
221
     * $GLOBALS["SIM_ACCESS_TIME"] is used for date.
222
     *
223
     * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA'].
224
     * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection)
225
     * @return string WHERE clause part
226
     * @internal should only be used from within TYPO3 Core, but DefaultRestrictionHandler is recommended as alternative
227
     */
228
    public static function BEenableFields($table, $inv = false)
229
    {
230
        $ctrl = $GLOBALS['TCA'][$table]['ctrl'] ?? [];
231
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
232
            ->getConnectionForTable($table)
233
            ->getExpressionBuilder();
234
        $query = $expressionBuilder->andX();
235
        $invQuery = $expressionBuilder->orX();
236
237
        $ctrl += [
238
            'enablecolumns' => [],
239
        ];
240
241
        if (is_array($ctrl)) {
242
            if ($ctrl['enablecolumns']['disabled'] ?? false) {
243
                $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
244
                $query->add($expressionBuilder->eq($field, 0));
245
                $invQuery->add($expressionBuilder->neq($field, 0));
246
            }
247
            if ($ctrl['enablecolumns']['starttime'] ?? false) {
248
                $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
249
                $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']));
250
                $invQuery->add(
251
                    $expressionBuilder->andX(
252
                        $expressionBuilder->neq($field, 0),
253
                        $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
254
                    )
255
                );
256
            }
257
            if ($ctrl['enablecolumns']['endtime'] ?? false) {
258
                $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
259
                $query->add(
260
                    $expressionBuilder->orX(
261
                        $expressionBuilder->eq($field, 0),
262
                        $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
263
                    )
264
                );
265
                $invQuery->add(
266
                    $expressionBuilder->andX(
267
                        $expressionBuilder->neq($field, 0),
268
                        $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
269
                    )
270
                );
271
            }
272
        }
273
274
        if ($query->count() === 0) {
275
            return '';
276
        }
277
278
        return ' AND ' . ($inv ? $invQuery : $query);
279
    }
280
281
    /**
282
     * Fetches the localization for a given record.
283
     *
284
     * @param string $table Table name present in $GLOBALS['TCA']
285
     * @param int $uid The uid of the record
286
     * @param int $language The uid of the language record in sys_language
287
     * @param string $andWhereClause Optional additional WHERE clause (default: '')
288
     * @return mixed Multidimensional array with selected records, empty array if none exists and FALSE if table is not localizable
289
     */
290
    public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
291
    {
292
        $recordLocalization = false;
293
294
        if (self::isTableLocalizable($table)) {
295
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
296
297
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
298
                ->getQueryBuilderForTable($table);
299
            $queryBuilder->getRestrictions()
300
                ->removeAll()
301
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
302
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace ?? 0));
303
304
            $queryBuilder->select('*')
305
                ->from($table)
306
                ->where(
307
                    $queryBuilder->expr()->eq(
308
                        $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'],
309
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
310
                    ),
311
                    $queryBuilder->expr()->eq(
312
                        $tcaCtrl['languageField'],
313
                        $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
314
                    )
315
                )
316
                ->setMaxResults(1);
317
318
            if ($andWhereClause) {
319
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
320
            }
321
322
            $recordLocalization = $queryBuilder->execute()->fetchAllAssociative();
323
        }
324
325
        return $recordLocalization;
326
    }
327
328
    /*******************************************
329
     *
330
     * Page tree, TCA related
331
     *
332
     *******************************************/
333
    /**
334
     * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id
335
     * ($uid) and back to the root.
336
     * By default deleted pages are filtered.
337
     * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known
338
     * from the frontend where the rootline stops when a root-template is found.
339
     *
340
     * @param int $uid Page id for which to create the root line.
341
     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that
342
     *          stops the process if we meet a page, the user has no reading access to.
343
     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is
344
     *          usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
345
     * @param string[] $additionalFields Additional Fields to select for rootline records
346
     * @return array Root line array, all the way to the page tree root uid=0 (or as far as $clause allows!), including the page given as $uid
347
     */
348
    public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = [])
349
    {
350
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
351
        $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: [];
352
        $output = [];
353
        $pid = $uid;
354
        $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . md5(implode(',', $additionalFields)) : '');
355
        if (is_array($beGetRootLineCache[$ident] ?? false)) {
356
            $output = $beGetRootLineCache[$ident];
357
        } else {
358
            $loopCheck = 100;
359
            $theRowArray = [];
360
            while ($uid != 0 && $loopCheck) {
361
                $loopCheck--;
362
                $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields);
363
                if (is_array($row)) {
364
                    $uid = $row['pid'];
365
                    $theRowArray[] = $row;
366
                } else {
367
                    break;
368
                }
369
            }
370
            $fields = [
371
                'uid',
372
                'pid',
373
                'title',
374
                'doktype',
375
                'slug',
376
                'tsconfig_includes',
377
                'TSconfig',
378
                'is_siteroot',
379
                't3ver_oid',
380
                't3ver_wsid',
381
                't3ver_state',
382
                't3ver_stage',
383
                'backend_layout_next_level',
384
                'hidden',
385
                'starttime',
386
                'endtime',
387
                'fe_group',
388
                'nav_hide',
389
                'content_from_pid',
390
                'module',
391
                'extendToSubpages',
392
            ];
393
            $fields = array_merge($fields, $additionalFields);
394
            $rootPage = array_fill_keys($fields, null);
395
            if ($uid == 0) {
396
                $rootPage['uid'] = 0;
397
                $theRowArray[] = $rootPage;
398
            }
399
            $c = count($theRowArray);
400
            foreach ($theRowArray as $val) {
401
                $c--;
402
                $output[$c] = array_intersect_key($val, $rootPage);
403
                if (isset($val['_ORIG_pid'])) {
404
                    $output[$c]['_ORIG_pid'] = $val['_ORIG_pid'];
405
                }
406
            }
407
            $beGetRootLineCache[$ident] = $output;
408
            $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache);
409
        }
410
        return $output;
411
    }
412
413
    /**
414
     * Gets the cached page record for the rootline
415
     *
416
     * @param int $uid Page id for which to create the root line.
417
     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that stops the process if we meet a page, the user has no reading access to.
418
     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
419
     * @param string[] $additionalFields AdditionalFields to fetch from the root line
420
     * @return array Cached page record for the rootline
421
     * @see BEgetRootLine
422
     */
423
    protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = [])
424
    {
425
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
426
        $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: [];
427
        $statementCacheIdent = md5($clause . ($additionalFields ? '-' . implode(',', $additionalFields) : ''));
428
        $ident = $uid . '-' . $workspaceOL . '-' . $statementCacheIdent;
429
        if (is_array($pageForRootlineCache[$ident] ?? false)) {
430
            $row = $pageForRootlineCache[$ident];
431
        } else {
432
            $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
433
            if (!$statement) {
434
                $queryBuilder = static::getQueryBuilderForTable('pages');
435
                $queryBuilder->getRestrictions()
436
                             ->removeAll()
437
                             ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
438
439
                $queryBuilder
440
                    ->select(
441
                        'pid',
442
                        'uid',
443
                        'title',
444
                        'doktype',
445
                        'slug',
446
                        'tsconfig_includes',
447
                        'TSconfig',
448
                        'is_siteroot',
449
                        't3ver_oid',
450
                        't3ver_wsid',
451
                        't3ver_state',
452
                        't3ver_stage',
453
                        'backend_layout_next_level',
454
                        'hidden',
455
                        'starttime',
456
                        'endtime',
457
                        'fe_group',
458
                        'nav_hide',
459
                        'content_from_pid',
460
                        'module',
461
                        'extendToSubpages',
462
                        ...$additionalFields
463
                    )
464
                    ->from('pages')
465
                    ->where(
466
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)),
467
                        QueryHelper::stripLogicalOperatorPrefix($clause)
468
                    );
469
                $statement = $queryBuilder->execute();
470
                if (class_exists(\Doctrine\DBAL\ForwardCompatibility\Result::class) && $statement instanceof \Doctrine\DBAL\ForwardCompatibility\Result) {
471
                    $statement = $statement->getIterator();
472
                }
473
                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement);
474
            } else {
475
                $statement->bindValue(1, (int)$uid);
476
                $statement->execute();
477
            }
478
            $row = $statement->fetchAssociative();
479
            $statement->free();
480
481
            if ($row) {
482
                if ($workspaceOL) {
483
                    self::workspaceOL('pages', $row);
484
                }
485
                if (is_array($row)) {
486
                    $pageForRootlineCache[$ident] = $row;
487
                    $runtimeCache->set('backendUtilityPageForRootLine', $pageForRootlineCache);
488
                }
489
            }
490
        }
491
        return $row;
492
    }
493
494
    /**
495
     * Opens the page tree to the specified page id
496
     *
497
     * @param int $pid Page id.
498
     * @param bool $clearExpansion If set, then other open branches are closed.
499
     * @internal should only be used from within TYPO3 Core
500
     */
501
    public static function openPageTree($pid, $clearExpansion)
502
    {
503
        $beUser = static::getBackendUserAuthentication();
504
        // Get current expansion data:
505
        if ($clearExpansion) {
506
            $expandedPages = [];
507
        } else {
508
            $expandedPages = $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'] ?? [];
509
        }
510
        // Get rootline:
511
        $rL = self::BEgetRootLine($pid);
512
        // First, find out what mount index to use (if more than one DB mount exists):
513
        $mountIndex = 0;
514
        $mountKeys = $beUser->returnWebmounts();
515
516
        foreach ($rL as $rLDat) {
517
            if (isset($mountKeys[$rLDat['uid']])) {
518
                $mountIndex = $mountKeys[$rLDat['uid']];
519
                break;
520
            }
521
        }
522
        // Traverse rootline and open paths:
523
        foreach ($rL as $rLDat) {
524
            $expandedPages[$mountIndex . '_' . $rLDat['uid']] = '1';
525
        }
526
        // Write back:
527
        $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'] = $expandedPages;
528
        $beUser->writeUC();
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Core\Authentic...thentication::writeUC() has been deprecated. ( Ignorable by Annotation )

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

528
        /** @scrutinizer ignore-deprecated */ $beUser->writeUC();
Loading history...
529
    }
530
531
    /**
532
     * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage"
533
     * Each part of the path will be limited to $titleLimit characters
534
     * Deleted pages are filtered out.
535
     *
536
     * @param int $uid Page uid for which to create record path
537
     * @param string $clause Clause is additional where clauses, eg.
538
     * @param int $titleLimit Title limit
539
     * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so)
540
     * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set.
541
     */
542
    public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0)
543
    {
544
        if (!$titleLimit) {
545
            $titleLimit = 1000;
546
        }
547
        $output = $fullOutput = '/';
548
        $clause = trim($clause);
549
        if ($clause !== '' && strpos($clause, 'AND') !== 0) {
550
            $clause = 'AND ' . $clause;
551
        }
552
        $data = self::BEgetRootLine($uid, $clause, true);
553
        foreach ($data as $record) {
554
            if ($record['uid'] === 0) {
555
                continue;
556
            }
557
            $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output;
558
            if ($fullTitleLimit) {
559
                $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput;
560
            }
561
        }
562
        if ($fullTitleLimit) {
563
            return [$output, $fullOutput];
564
        }
565
        return $output;
566
    }
567
568
    /**
569
     * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA'].
570
     *
571
     * @param string $table The table to check
572
     * @return bool Whether a table is localizable
573
     */
574
    public static function isTableLocalizable($table)
575
    {
576
        $isLocalizable = false;
577
        if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) {
578
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
579
            $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField'];
580
        }
581
        return $isLocalizable;
582
    }
583
584
    /**
585
     * Returns a page record (of page with $id) with an extra field "_thePath" set to the record path IF the WHERE clause, $perms_clause, selects the record. Thus is works as an access check that returns a page record if access was granted, otherwise not.
586
     * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin.
587
     * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause)
588
     *
589
     * @param int $id Page uid for which to check read-access
590
     * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1);
591
     * @return array|false Returns page record if OK, otherwise FALSE.
592
     */
593
    public static function readPageAccess($id, $perms_clause)
594
    {
595
        if ((string)$id !== '') {
596
            $id = (int)$id;
597
            if (!$id) {
598
                if (static::getBackendUserAuthentication()->isAdmin()) {
599
                    return ['_thePath' => '/'];
600
                }
601
            } else {
602
                $pageinfo = self::getRecord('pages', $id, '*', $perms_clause);
603
                if (($pageinfo['uid'] ?? false) && static::getBackendUserAuthentication()->isInWebMount($pageinfo, $perms_clause)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getBackendUserAu...ageinfo, $perms_clause) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Deprecated Code introduced by
The function TYPO3\CMS\Core\Authentic...ication::isInWebMount() has been deprecated. ( Ignorable by Annotation )

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

603
                if (($pageinfo['uid'] ?? false) && /** @scrutinizer ignore-deprecated */ static::getBackendUserAuthentication()->isInWebMount($pageinfo, $perms_clause)) {
Loading history...
604
                    self::workspaceOL('pages', $pageinfo);
605
                    if (is_array($pageinfo)) {
606
                        [$pageinfo['_thePath'], $pageinfo['_thePathFull']] = self::getRecordPath((int)$pageinfo['uid'], $perms_clause, 15, 1000);
607
                        return $pageinfo;
608
                    }
609
                }
610
            }
611
        }
612
        return false;
613
    }
614
615
    /**
616
     * Returns the "type" value of $rec from $table which can be used to look up the correct "types" rendering section in $GLOBALS['TCA']
617
     * If no "type" field is configured in the "ctrl"-section of the $GLOBALS['TCA'] for the table, zero is used.
618
     * If zero is not an index in the "types" section of $GLOBALS['TCA'] for the table, then the $fieldValue returned will default to 1 (no matter if that is an index or not)
619
     *
620
     * Note: This method is very similar to the type determination of FormDataProvider/DatabaseRecordTypeValue,
621
     * however, it has two differences:
622
     * 1) The method in TCEForms also takes care of localization (which is difficult to do here as the whole infrastructure for language overlays is only in TCEforms).
623
     * 2) The $row array looks different in TCEForms, as in there it's not the raw record but the prepared data from other providers is handled, which changes e.g. how "select"
624
     * and "group" field values are stored, which makes different processing of the "foreign pointer field" type field variant necessary.
625
     *
626
     * @param string $table Table name present in TCA
627
     * @param array $row Record from $table
628
     * @throws \RuntimeException
629
     * @return string Field value
630
     */
631
    public static function getTCAtypeValue($table, $row)
632
    {
633
        $typeNum = 0;
634
        if ($GLOBALS['TCA'][$table] ?? false) {
635
            $field = $GLOBALS['TCA'][$table]['ctrl']['type'] ?? '';
636
            if (str_contains($field, ':')) {
637
                [$pointerField, $foreignTableTypeField] = explode(':', $field);
638
                // Get field value from database if field is not in the $row array
639
                if (!isset($row[$pointerField])) {
640
                    $localRow = self::getRecord($table, $row['uid'], $pointerField);
641
                    $foreignUid = $localRow[$pointerField];
642
                } else {
643
                    $foreignUid = $row[$pointerField];
644
                }
645
                if ($foreignUid) {
646
                    $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
647
                    $relationType = $fieldConfig['type'];
648
                    if ($relationType === 'select' || $relationType === 'category') {
649
                        $foreignTable = $fieldConfig['foreign_table'];
650
                    } elseif ($relationType === 'group') {
651
                        $allowedTables = explode(',', $fieldConfig['allowed']);
652
                        $foreignTable = $allowedTables[0];
653
                    } else {
654
                        throw new \RuntimeException(
655
                            'TCA foreign field pointer fields are only allowed to be used with group or select field types.',
656
                            1325862240
657
                        );
658
                    }
659
                    $foreignRow = self::getRecord($foreignTable, $foreignUid, $foreignTableTypeField);
660
                    if ($foreignRow[$foreignTableTypeField] ?? false) {
661
                        $typeNum = $foreignRow[$foreignTableTypeField];
662
                    }
663
                }
664
            } else {
665
                $typeNum = $row[$field] ?? 0;
666
            }
667
            // If that value is an empty string, set it to "0" (zero)
668
            if (empty($typeNum)) {
669
                $typeNum = 0;
670
            }
671
        }
672
        // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
673
        if (!isset($GLOBALS['TCA'][$table]['types'][$typeNum]) || !$GLOBALS['TCA'][$table]['types'][$typeNum]) {
674
            $typeNum = isset($GLOBALS['TCA'][$table]['types']['0']) ? 0 : 1;
675
        }
676
        // Force to string. Necessary for eg '-1' to be recognized as a type value.
677
        $typeNum = (string)$typeNum;
678
        return $typeNum;
679
    }
680
681
    /*******************************************
682
     *
683
     * TypoScript related
684
     *
685
     *******************************************/
686
    /**
687
     * Returns the Page TSconfig for page with id, $id
688
     *
689
     * @param int $id Page uid for which to create Page TSconfig
690
     * @return array Page TSconfig
691
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
692
     */
693
    public static function getPagesTSconfig($id)
694
    {
695
        $id = (int)$id;
696
697
        $cache = self::getRuntimeCache();
698
        $pagesTsConfigIdToHash = $cache->get('pagesTsConfigIdToHash' . $id);
699
        if ($pagesTsConfigIdToHash !== false) {
700
            return $cache->get('pagesTsConfigHashToContent' . $pagesTsConfigIdToHash);
701
        }
702
703
        $rootLine = self::BEgetRootLine($id, '', true);
704
        // Order correctly
705
        ksort($rootLine);
706
707
        try {
708
            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id);
709
        } catch (SiteNotFoundException $exception) {
710
            $site = null;
711
        }
712
713
        // Load PageTS from all pages of the rootLine
714
        $pageTs = GeneralUtility::makeInstance(PageTsConfigLoader::class)->load($rootLine);
715
716
        // Parse the PageTS into an array, also applying conditions
717
        $parser = GeneralUtility::makeInstance(
718
            PageTsConfigParser::class,
719
            GeneralUtility::makeInstance(TypoScriptParser::class),
720
            GeneralUtility::makeInstance(CacheManager::class)->getCache('hash')
721
        );
722
        $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, null, $id, $rootLine);
723
        $tsConfig = $parser->parse($pageTs, $matcher, $site);
724
        $cacheHash = md5((string)json_encode($tsConfig));
725
726
        // Get User TSconfig overlay, if no backend user is logged-in, this needs to be checked as well
727
        if (static::getBackendUserAuthentication()) {
728
            $userTSconfig = static::getBackendUserAuthentication()->getTSConfig() ?? [];
729
        } else {
730
            $userTSconfig = [];
731
        }
732
733
        if (is_array($userTSconfig['page.'] ?? null)) {
734
            // Override page TSconfig with user TSconfig
735
            ArrayUtility::mergeRecursiveWithOverrule($tsConfig, $userTSconfig['page.']);
736
            $cacheHash .= '_user' . static::getBackendUserAuthentication()->user['uid'];
737
        }
738
739
        // Many pages end up with the same ts config. To reduce memory usage, the cache
740
        // entries are a linked list: One or more pids point to content hashes which then
741
        // contain the cached content.
742
        $cache->set('pagesTsConfigHashToContent' . $cacheHash, $tsConfig, ['pagesTsConfig']);
743
        $cache->set('pagesTsConfigIdToHash' . $id, $cacheHash, ['pagesTsConfig']);
744
745
        return $tsConfig;
746
    }
747
748
    /*******************************************
749
     *
750
     * Users / Groups related
751
     *
752
     *******************************************/
753
    /**
754
     * Returns an array with be_users records of all user NOT DELETED sorted by their username
755
     * Keys in the array is the be_users uid
756
     *
757
     * @param string $fields Optional $fields list (default: username,usergroup,uid) can be used to set the selected fields
758
     * @param string $where Optional $where clause (fx. "AND username='pete'") can be used to limit query
759
     * @return array
760
     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
761
     */
762
    public static function getUserNames($fields = 'username,usergroup,uid', $where = '')
763
    {
764
        return self::getRecordsSortedByTitle(
765
            GeneralUtility::trimExplode(',', $fields, true),
766
            'be_users',
767
            'username',
768
            'AND pid=0 ' . $where
769
        );
770
    }
771
772
    /**
773
     * Returns an array with be_groups records (title, uid) of all groups NOT DELETED sorted by their title
774
     *
775
     * @param string $fields Field list
776
     * @param string $where WHERE clause
777
     * @return array
778
     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
779
     */
780
    public static function getGroupNames($fields = 'title,uid', $where = '')
781
    {
782
        return self::getRecordsSortedByTitle(
783
            GeneralUtility::trimExplode(',', $fields, true),
784
            'be_groups',
785
            'title',
786
            'AND pid=0 ' . $where
787
        );
788
    }
789
790
    /**
791
     * Returns an array of all non-deleted records of a table sorted by a given title field.
792
     * The value of the title field will be replaced by the return value
793
     * of self::getRecordTitle() before the sorting is performed.
794
     *
795
     * @param array $fields Fields to select
796
     * @param string $table Table name
797
     * @param string $titleField Field that will contain the record title
798
     * @param string $where Additional where clause
799
     * @return array Array of sorted records
800
     */
801
    protected static function getRecordsSortedByTitle(array $fields, $table, $titleField, $where = '')
802
    {
803
        $fieldsIndex = array_flip($fields);
804
        // Make sure the titleField is amongst the fields when getting sorted
805
        $fieldsIndex[$titleField] = 1;
806
807
        $result = [];
808
809
        $queryBuilder = static::getQueryBuilderForTable($table);
810
        $queryBuilder->getRestrictions()
811
            ->removeAll()
812
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
813
814
        $res = $queryBuilder
815
            ->select('*')
816
            ->from($table)
817
            ->where(QueryHelper::stripLogicalOperatorPrefix($where))
818
            ->execute();
819
820
        while ($record = $res->fetchAssociative()) {
821
            // store the uid, because it might be unset if it's not among the requested $fields
822
            $recordId = $record['uid'];
823
            $record[$titleField] = self::getRecordTitle($table, $record);
824
825
            // include only the requested fields in the result
826
            $result[$recordId] = array_intersect_key($record, $fieldsIndex);
827
        }
828
829
        // sort records by $sortField. This is not done in the query because the title might have been overwritten by
830
        // self::getRecordTitle();
831
        return ArrayUtility::sortArraysByKey($result, $titleField);
832
    }
833
834
    /*******************************************
835
     *
836
     * Output related
837
     *
838
     *******************************************/
839
    /**
840
     * Returns the difference in days between input $tstamp and $EXEC_TIME
841
     *
842
     * @param int $tstamp Time stamp, seconds
843
     * @return int
844
     */
845
    public static function daysUntil($tstamp)
846
    {
847
        $delta_t = $tstamp - $GLOBALS['EXEC_TIME'];
848
        return ceil($delta_t / (3600 * 24));
849
    }
850
851
    /**
852
     * Returns $tstamp formatted as "ddmmyy" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'])
853
     *
854
     * @param int $tstamp Time stamp, seconds
855
     * @return string Formatted time
856
     */
857
    public static function date($tstamp)
858
    {
859
        return date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$tstamp);
860
    }
861
862
    /**
863
     * Returns $tstamp formatted as "ddmmyy hhmm" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] AND $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'])
864
     *
865
     * @param int $value Time stamp, seconds
866
     * @return string Formatted time
867
     */
868
    public static function datetime($value)
869
    {
870
        return date(
871
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
872
            $value
873
        );
874
    }
875
876
    /**
877
     * Returns $value (in seconds) formatted as hh:mm:ss
878
     * For instance $value = 3600 + 60*2 + 3 should return "01:02:03"
879
     *
880
     * @param int $value Time stamp, seconds
881
     * @param bool $withSeconds Output hh:mm:ss. If FALSE: hh:mm
882
     * @return string Formatted time
883
     */
884
    public static function time($value, $withSeconds = true)
885
    {
886
        return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value);
887
    }
888
889
    /**
890
     * Returns the "age" in minutes / hours / days / years of the number of $seconds inputted.
891
     *
892
     * @param int $seconds Seconds could be the difference of a certain timestamp and time()
893
     * @param string $labels Labels should be something like ' min| hrs| days| yrs| min| hour| day| year'. This value is typically delivered by this function call: $GLOBALS["LANG"]->sL("LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears")
894
     * @return string Formatted time
895
     */
896
    public static function calcAge($seconds, $labels = 'min|hrs|days|yrs|min|hour|day|year')
897
    {
898
        $labelArr = GeneralUtility::trimExplode('|', $labels, true);
899
        $absSeconds = abs($seconds);
900
        $sign = $seconds < 0 ? -1 : 1;
901
        if ($absSeconds < 3600) {
902
            $val = round($absSeconds / 60);
903
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[4] : $labelArr[0]);
904
        } elseif ($absSeconds < 24 * 3600) {
905
            $val = round($absSeconds / 3600);
906
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[5] : $labelArr[1]);
907
        } elseif ($absSeconds < 365 * 24 * 3600) {
908
            $val = round($absSeconds / (24 * 3600));
909
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[6] : $labelArr[2]);
910
        } else {
911
            $val = round($absSeconds / (365 * 24 * 3600));
912
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[7] : $labelArr[3]);
913
        }
914
        return $seconds;
915
    }
916
917
    /**
918
     * Returns a formatted timestamp if $tstamp is set.
919
     * The date/datetime will be followed by the age in parenthesis.
920
     *
921
     * @param int $tstamp Time stamp, seconds
922
     * @param int $prefix 1/-1 depending on polarity of age.
923
     * @param string $date $date=="date" will yield "dd:mm:yy" formatting, otherwise "dd:mm:yy hh:mm
924
     * @return string
925
     */
926
    public static function dateTimeAge($tstamp, $prefix = 1, $date = '')
927
    {
928
        if (!$tstamp) {
929
            return '';
930
        }
931
        $label = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
932
        $age = ' (' . self::calcAge($prefix * ($GLOBALS['EXEC_TIME'] - $tstamp), $label) . ')';
933
        return ($date === 'date' ? self::date($tstamp) : self::datetime($tstamp)) . $age;
934
    }
935
936
    /**
937
     * Resolves file references for a given record.
938
     *
939
     * @param string $tableName Name of the table of the record
940
     * @param string $fieldName Name of the field of the record
941
     * @param array $element Record data
942
     * @param int|null $workspaceId Workspace to fetch data for
943
     * @return \TYPO3\CMS\Core\Resource\FileReference[]|null
944
     */
945
    public static function resolveFileReferences($tableName, $fieldName, $element, $workspaceId = null)
946
    {
947
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
948
            return null;
949
        }
950
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
951
        if (empty($configuration['type']) || $configuration['type'] !== 'inline'
952
            || empty($configuration['foreign_table']) || $configuration['foreign_table'] !== 'sys_file_reference'
953
        ) {
954
            return null;
955
        }
956
957
        $fileReferences = [];
958
        /** @var RelationHandler $relationHandler */
959
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
960
        if ($workspaceId !== null) {
961
            $relationHandler->setWorkspaceId($workspaceId);
962
        }
963
        $relationHandler->start(
964
            $element[$fieldName],
965
            $configuration['foreign_table'],
966
            $configuration['MM'] ?? '',
967
            $element['uid'],
968
            $tableName,
969
            $configuration
970
        );
971
        $relationHandler->processDeletePlaceholder();
972
        $referenceUids = $relationHandler->tableArray[$configuration['foreign_table']];
973
974
        foreach ($referenceUids as $referenceUid) {
975
            try {
976
                $fileReference = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject(
977
                    $referenceUid,
978
                    [],
979
                    $workspaceId === 0
980
                );
981
                $fileReferences[$fileReference->getUid()] = $fileReference;
982
            } catch (FileDoesNotExistException $e) {
983
                /**
984
                 * We just catch the exception here
985
                 * Reasoning: There is nothing an editor or even admin could do
986
                 */
987
            } catch (\InvalidArgumentException $e) {
988
                /**
989
                 * The storage does not exist anymore
990
                 * Log the exception message for admins as they maybe can restore the storage
991
                 */
992
                self::getLogger()->error($e->getMessage(), [
993
                    'table' => $tableName,
994
                    'fieldName' => $fieldName,
995
                    'referenceUid' => $referenceUid,
996
                    'exception' => $e,
997
                ]);
998
            }
999
        }
1000
1001
        return $fileReferences;
1002
    }
1003
1004
    /**
1005
     * Returns a linked image-tag for thumbnail(s)/fileicons/truetype-font-previews from a database row with sys_file_references
1006
     * All $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] extension are made to thumbnails + ttf file (renders font-example)
1007
     * Thumbnails are linked to ShowItemController (/thumbnails route)
1008
     *
1009
     * @param array $row Row is the database row from the table, $table.
1010
     * @param string $table Table name for $row (present in TCA)
1011
     * @param string $field Field is pointing to the connecting field of sys_file_references
1012
     * @param string $backPath Back path prefix for image tag src="" field
1013
     * @param string $thumbScript UNUSED since FAL
1014
     * @param string $uploaddir UNUSED since FAL
1015
     * @param int $abs UNUSED
1016
     * @param string $tparams Optional: $tparams is additional attributes for the image tags
1017
     * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default.
1018
     * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup
1019
     * @return string Thumbnail image tag.
1020
     */
1021
    public static function thumbCode(
1022
        $row,
1023
        $table,
1024
        $field,
1025
        $backPath = '',
1026
        $thumbScript = '',
1027
        $uploaddir = null,
1028
        $abs = 0,
1029
        $tparams = '',
1030
        $size = '',
1031
        $linkInfoPopup = true
1032
    ) {
1033
        $size = (int)(trim((string)$size) ?: 64);
1034
        $targetDimension = new ImageDimension($size, $size);
1035
        $thumbData = '';
1036
        $fileReferences = static::resolveFileReferences($table, $field, $row);
1037
        // FAL references
1038
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1039
        if ($fileReferences !== null) {
1040
            foreach ($fileReferences as $fileReferenceObject) {
1041
                // Do not show previews of hidden references
1042
                if ($fileReferenceObject->getProperty('hidden')) {
1043
                    continue;
1044
                }
1045
                $fileObject = $fileReferenceObject->getOriginalFile();
1046
1047
                if ($fileObject->isMissing()) {
1048
                    $thumbData .= '<span class="label label-danger">'
1049
                        . htmlspecialchars(
1050
                            static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')
1051
                        )
1052
                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1053
                    continue;
1054
                }
1055
1056
                // Preview web image or media elements
1057
                if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
1058
                    && $fileReferenceObject->getOriginalFile()->isImage()
1059
                ) {
1060
                    $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
1061
                    $cropArea = $cropVariantCollection->getCropArea();
1062
                    $taskType = ProcessedFile::CONTEXT_IMAGEPREVIEW;
1063
                    $processingConfiguration = [
1064
                        'width' => $targetDimension->getWidth(),
1065
                        'height' => $targetDimension->getHeight(),
1066
                    ];
1067
                    if (!$cropArea->isEmpty()) {
1068
                        $taskType = ProcessedFile::CONTEXT_IMAGECROPSCALEMASK;
1069
                        $processingConfiguration = [
1070
                            'maxWidth' => $targetDimension->getWidth(),
1071
                            'maxHeight' => $targetDimension->getHeight(),
1072
                            'crop' => $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
1073
                        ];
1074
                    }
1075
                    $processedImage = $fileObject->process($taskType, $processingConfiguration);
1076
                    $attributes = [
1077
                        'src' => $processedImage->getPublicUrl() ?? '',
1078
                        'width' => $processedImage->getProperty('width'),
1079
                        'height' => $processedImage->getProperty('height'),
1080
                        'alt' => $fileReferenceObject->getName(),
1081
                    ];
1082
                    $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . $tparams . '/>';
1083
                } else {
1084
                    // Icon
1085
                    $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1086
                        . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1087
                        . '</span>';
1088
                }
1089
                if ($linkInfoPopup) {
1090
                    // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
1091
                    $attributes = GeneralUtility::implodeAttributes([
1092
                        'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
1093
                        'data-dispatch-args-list' => '_FILE,' . (int)$fileObject->getUid(),
1094
                    ], true);
1095
                    $thumbData .= '<a href="#" ' . $attributes . '>' . $imgTag . '</a> ';
1096
                } else {
1097
                    $thumbData .= $imgTag;
1098
                }
1099
            }
1100
        }
1101
        return $thumbData;
1102
    }
1103
1104
    /**
1105
     * @param int $fileId
1106
     * @param array $configuration
1107
     * @return string
1108
     */
1109
    public static function getThumbnailUrl(int $fileId, array $configuration): string
1110
    {
1111
        $taskType = $configuration['_context'] ?? ProcessedFile::CONTEXT_IMAGEPREVIEW;
1112
        unset($configuration['_context']);
1113
1114
        return GeneralUtility::makeInstance(ResourceFactory::class)
1115
                ->getFileObject($fileId)
1116
                ->process($taskType, $configuration)
1117
                ->getPublicUrl(true);
1118
    }
1119
1120
    /**
1121
     * Returns title-attribute information for a page-record informing about id, doktype, hidden, starttime, endtime, fe_group etc.
1122
     *
1123
     * @param array $row Input must be a page row ($row) with the proper fields set (be sure - send the full range of fields for the table)
1124
     * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4)
1125
     * @param bool $includeAttrib If $includeAttrib is set, then the 'title=""' attribute is wrapped about the return value, which is in any case htmlspecialchar()'ed already
1126
     * @return string
1127
     */
1128
    public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true)
1129
    {
1130
        $lang = static::getLanguageService();
1131
        $parts = [];
1132
        $parts[] = 'id=' . $row['uid'];
1133
        if ($row['uid'] === 0) {
1134
            $out = htmlspecialchars($parts[0]);
1135
            return $includeAttrib ? 'title="' . $out . '"' : $out;
1136
        }
1137
        switch (VersionState::cast($row['t3ver_state'])) {
1138
            case new VersionState(VersionState::DELETE_PLACEHOLDER):
1139
                $parts[] = 'Deleted element!';
1140
                break;
1141
            case new VersionState(VersionState::MOVE_POINTER):
1142
                $parts[] = 'NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1143
                break;
1144
            case new VersionState(VersionState::NEW_PLACEHOLDER):
1145
                $parts[] = 'New element!';
1146
                break;
1147
        }
1148
        if ($row['doktype'] == PageRepository::DOKTYPE_LINK) {
1149
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label'] ?? '') . ' ' . ($row['url'] ?? '');
1150
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1151
            if ($perms_clause) {
1152
                $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20);
1153
            } else {
1154
                $row['shortcut'] = (int)($row['shortcut'] ?? 0);
1155
                $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title');
1156
                $label = ($lRec === null ? '' : $lRec['title']) . ' (id=' . $row['shortcut'] . ')';
0 ignored issues
show
introduced by
The condition $lRec === null is always false.
Loading history...
1157
            }
1158
            if (($row['shortcut_mode'] ?? 0) != PageRepository::SHORTCUT_MODE_NONE) {
1159
                $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' '
1160
                    . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode']));
1161
            }
1162
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . $label;
0 ignored issues
show
Bug introduced by
Are you sure $label of type array<integer,string>|string can be used in concatenation? ( Ignorable by Annotation )

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

1162
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . /** @scrutinizer ignore-type */ $label;
Loading history...
1163
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) {
1164
            if ((int)$row['mount_pid'] > 0) {
1165
                if ($perms_clause) {
1166
                    $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20);
1167
                } else {
1168
                    $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title');
1169
                    $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')';
1170
                }
1171
                $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label;
1172
                if ($row['mount_pid_ol'] ?? 0) {
1173
                    $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']);
1174
                }
1175
            } else {
1176
                $parts[] = $lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:no_mount_pid');
1177
            }
1178
        }
1179
        if ($row['nav_hide']) {
1180
            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide');
1181
        }
1182
        if ($row['hidden']) {
1183
            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden');
1184
        }
1185
        if ($row['starttime']) {
1186
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label'])
1187
                . ' ' . self::dateTimeAge($row['starttime'], -1, 'date');
1188
        }
1189
        if ($row['endtime']) {
1190
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' '
1191
                . self::dateTimeAge($row['endtime'], -1, 'date');
1192
        }
1193
        if ($row['fe_group']) {
1194
            $fe_groups = [];
1195
            foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) {
1196
                if ($fe_group < 0) {
1197
                    $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', (string)$fe_group));
1198
                } else {
1199
                    $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title');
1200
                    $fe_groups[] = $lRec['title'];
1201
                }
1202
            }
1203
            $label = implode(', ', $fe_groups);
1204
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label;
1205
        }
1206
        $out = htmlspecialchars(implode(' - ', $parts));
1207
        return $includeAttrib ? 'title="' . $out . '"' : $out;
1208
    }
1209
1210
    /**
1211
     * Returns the combined markup for Bootstraps tooltips
1212
     *
1213
     * @param array $row
1214
     * @param string $table
1215
     * @return string
1216
     */
1217
    public static function getRecordToolTip(array $row, $table = 'pages')
1218
    {
1219
        $toolTipText = self::getRecordIconAltText($row, $table);
1220
        $toolTipCode = 'data-bs-toggle="tooltip" title=" '
1221
            . str_replace(' - ', '<br>', $toolTipText)
1222
            . '" data-bs-html="true" data-bs-placement="right"';
1223
        return $toolTipCode;
1224
    }
1225
1226
    /**
1227
     * Returns title-attribute information for ANY record (from a table defined in TCA of course)
1228
     * The included information depends on features of the table, but if hidden, starttime, endtime and fe_group fields are configured for, information about the record status in regard to these features are is included.
1229
     * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page.
1230
     *
1231
     * @param array $row Table row; $row is a row from the table, $table
1232
     * @param string $table Table name
1233
     * @return string
1234
     */
1235
    public static function getRecordIconAltText($row, $table = 'pages')
1236
    {
1237
        if ($table === 'pages') {
1238
            $out = self::titleAttribForPages($row, '', false);
1239
        } else {
1240
            $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'] ?? ''))
1241
                ? ($row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] ?? '') . ' '
1242
                : '';
1243
            $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'] ?? [];
1244
            // Uid is added
1245
            $out .= 'id=' . ($row['uid'] ?? 0);
1246
            if (static::isTableWorkspaceEnabled($table)) {
1247
                switch (VersionState::cast($row['t3ver_state'] ?? null)) {
1248
                    case new VersionState(VersionState::DELETE_PLACEHOLDER):
1249
                        $out .= ' - Deleted element!';
1250
                        break;
1251
                    case new VersionState(VersionState::MOVE_POINTER):
1252
                        $out .= ' - NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1253
                        break;
1254
                    case new VersionState(VersionState::NEW_PLACEHOLDER):
1255
                        $out .= ' - New element!';
1256
                        break;
1257
                }
1258
            }
1259
            // Hidden
1260
            $lang = static::getLanguageService();
1261
            if ($ctrl['disabled'] ?? false) {
1262
                $out .= ($row[$ctrl['disabled']] ?? false) ? ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') : '';
1263
            }
1264
            if (($ctrl['starttime'] ?? false) && ($row[$ctrl['starttime']] ?? 0) > $GLOBALS['EXEC_TIME']) {
1265
                $out .= ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.starttime') . ':' . self::date($row[$ctrl['starttime']]) . ' (' . self::daysUntil($row[$ctrl['starttime']]) . ' ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.days') . ')';
1266
            }
1267
            if (($ctrl['endtime'] ?? false) && ($row[$ctrl['endtime']] ?? false)) {
1268
                $out .= ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.endtime') . ': ' . self::date($row[$ctrl['endtime']]) . ' (' . self::daysUntil($row[$ctrl['endtime']]) . ' ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.days') . ')';
1269
            }
1270
        }
1271
        return htmlspecialchars($out);
1272
    }
1273
1274
    /**
1275
     * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key
1276
     *
1277
     * @param string $table Table name, present in $GLOBALS['TCA']
1278
     * @param string $col Field name, present in $GLOBALS['TCA']
1279
     * @param string $key items-array value to match
1280
     * @return string Label for item entry
1281
     */
1282
    public static function getLabelFromItemlist($table, $col, $key)
1283
    {
1284
        // Check, if there is an "items" array:
1285
        if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) {
1286
            // Traverse the items-array...
1287
            foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) {
1288
                // ... and return the first found label where the value was equal to $key
1289
                if ((string)$v[1] === (string)$key) {
1290
                    return $v[0];
1291
                }
1292
            }
1293
        }
1294
        return '';
1295
    }
1296
1297
    /**
1298
     * Return the label of a field by additionally checking TsConfig values
1299
     *
1300
     * @param int $pageId Page id
1301
     * @param string $table Table name
1302
     * @param string $column Field Name
1303
     * @param string $key item value
1304
     * @return string Label for item entry
1305
     */
1306
    public static function getLabelFromItemListMerged($pageId, $table, $column, $key)
1307
    {
1308
        $pageTsConfig = static::getPagesTSconfig($pageId);
1309
        $label = '';
1310
        if (isset($pageTsConfig['TCEFORM.'])
1311
            && is_array($pageTsConfig['TCEFORM.'] ?? null)
1312
            && is_array($pageTsConfig['TCEFORM.'][$table . '.'] ?? null)
1313
            && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.'] ?? null)
1314
        ) {
1315
            if (is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'] ?? null)
1316
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key])
1317
            ) {
1318
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key];
1319
            } elseif (is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'] ?? null)
1320
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key])
1321
            ) {
1322
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key];
1323
            }
1324
        }
1325
        if (empty($label)) {
1326
            $tcaValue = self::getLabelFromItemlist($table, $column, $key);
1327
            if (!empty($tcaValue)) {
1328
                $label = $tcaValue;
1329
            }
1330
        }
1331
        return $label;
1332
    }
1333
1334
    /**
1335
     * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma.
1336
     * NOTE: this does not take itemsProcFunc into account
1337
     *
1338
     * @param string $table Table name, present in TCA
1339
     * @param string $column Field name
1340
     * @param string $keyList Key or comma-separated list of keys.
1341
     * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>)
1342
     * @return string Comma-separated list of localized labels
1343
     */
1344
    public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = [])
1345
    {
1346
        // Check if there is an "items" array
1347
        if (
1348
            !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1349
            || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1350
            || $keyList === ''
1351
        ) {
1352
            return '';
1353
        }
1354
1355
        $keys = GeneralUtility::trimExplode(',', $keyList, true);
1356
        $labels = [];
1357
        // Loop on all selected values
1358
        foreach ($keys as $key) {
1359
            $label = null;
1360
            if ($columnTsConfig) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columnTsConfig 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...
1361
                // Check if label has been defined or redefined via pageTsConfig
1362
                if (isset($columnTsConfig['addItems.'][$key])) {
1363
                    $label = $columnTsConfig['addItems.'][$key];
1364
                } elseif (isset($columnTsConfig['altLabels.'][$key])) {
1365
                    $label = $columnTsConfig['altLabels.'][$key];
1366
                }
1367
            }
1368
            if ($label === null) {
1369
                // Otherwise lookup the label in TCA items list
1370
                foreach ($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'] as $itemConfiguration) {
1371
                    [$currentLabel, $currentKey] = $itemConfiguration;
1372
                    if ((string)$key === (string)$currentKey) {
1373
                        $label = $currentLabel;
1374
                        break;
1375
                    }
1376
                }
1377
            }
1378
            if ($label !== null) {
1379
                $labels[] = static::getLanguageService()->sL($label);
1380
            }
1381
        }
1382
        return implode(', ', $labels);
1383
    }
1384
1385
    /**
1386
     * Returns the label-value for fieldname $col in table, $table
1387
     * If $printAllWrap is set (to a "wrap") then it's wrapped around the $col value IF THE COLUMN $col DID NOT EXIST in TCA!, eg. $printAllWrap = '<strong>|</strong>' and the fieldname was 'not_found_field' then the return value would be '<strong>not_found_field</strong>'
1388
     *
1389
     * @param string $table Table name, present in $GLOBALS['TCA']
1390
     * @param string $col Field name
1391
     * @return string or NULL if $col is not found in the TCA table
1392
     */
1393
    public static function getItemLabel($table, $col)
1394
    {
1395
        return $GLOBALS['TCA'][$table]['columns'][$col]['label'] ?? null;
1396
    }
1397
1398
    /**
1399
     * Returns the "title"-value in record, $row, from table, $table
1400
     * The field(s) from which the value is taken is determined by the "ctrl"-entries 'label', 'label_alt' and 'label_alt_force'
1401
     *
1402
     * @param string $table Table name, present in TCA
1403
     * @param array $row Row from table
1404
     * @param bool $prep If set, result is prepared for output: The output is cropped to a limited length (depending on BE_USER->uc['titleLen']) and if no value is found for the title, '<em>[No title]</em>' is returned (localized). Further, the output is htmlspecialchars()'ed
1405
     * @param bool $forceResult If set, the function always returns an output. If no value is found for the title, '[No title]' is returned (localized).
1406
     * @return string
1407
     */
1408
    public static function getRecordTitle($table, $row, $prep = false, $forceResult = true)
1409
    {
1410
        $params = [];
1411
        $recordTitle = '';
1412
        if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table])) {
1413
            // If configured, call userFunc
1414
            if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'])) {
1415
                $params['table'] = $table;
1416
                $params['row'] = $row;
1417
                $params['title'] = '';
1418
                $params['options'] = $GLOBALS['TCA'][$table]['ctrl']['label_userFunc_options'] ?? [];
1419
1420
                // Create NULL-reference
1421
                $null = null;
1422
                GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'], $params, $null);
1423
                $recordTitle = $params['title'];
1424
            } else {
1425
                // No userFunc: Build label
1426
                $ctrlLabel = $GLOBALS['TCA'][$table]['ctrl']['label'] ?? '';
1427
                $recordTitle = self::getProcessedValue(
1428
                    $table,
1429
                    $ctrlLabel,
1430
                    (string)($row[$ctrlLabel] ?? ''),
1431
                    0,
1432
                    false,
1433
                    false,
1434
                    $row['uid'] ?? null,
1435
                    $forceResult
1436
                ) ?? '';
1437
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])
1438
                    && (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) || $recordTitle === '')
1439
                ) {
1440
                    $altFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
1441
                    $tA = [];
1442
                    if (!empty($recordTitle)) {
1443
                        $tA[] = $recordTitle;
1444
                    }
1445
                    foreach ($altFields as $fN) {
1446
                        $recordTitle = trim(strip_tags((string)($row[$fN] ?? '')));
1447
                        if ($recordTitle !== '') {
1448
                            $recordTitle = self::getProcessedValue($table, $fN, $recordTitle, 0, false, false, $row['uid'] ?? 0);
1449
                            if (!($GLOBALS['TCA'][$table]['ctrl']['label_alt_force'] ?? false)) {
1450
                                break;
1451
                            }
1452
                            $tA[] = $recordTitle;
1453
                        }
1454
                    }
1455
                    if ($GLOBALS['TCA'][$table]['ctrl']['label_alt_force'] ?? false) {
1456
                        $recordTitle = implode(', ', $tA);
1457
                    }
1458
                }
1459
            }
1460
            // If the current result is empty, set it to '[No title]' (localized) and prepare for output if requested
1461
            if ($prep || $forceResult) {
1462
                if ($prep) {
1463
                    $recordTitle = self::getRecordTitlePrep($recordTitle);
1464
                }
1465
                if (trim($recordTitle) === '') {
0 ignored issues
show
Bug introduced by
It seems like $recordTitle can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1465
                if (trim(/** @scrutinizer ignore-type */ $recordTitle) === '') {
Loading history...
1466
                    $recordTitle = self::getNoRecordTitle($prep);
1467
                }
1468
            }
1469
        }
1470
1471
        return $recordTitle;
1472
    }
1473
1474
    /**
1475
     * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>,
1476
     * which offers a tooltip with the original title when moving mouse over it.
1477
     *
1478
     * @param string $title The title string to be cropped
1479
     * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
1480
     * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
1481
     */
1482
    public static function getRecordTitlePrep($title, $titleLength = 0)
1483
    {
1484
        // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
1485
        if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
1486
            $titleLength = static::getBackendUserAuthentication()->uc['titleLen'];
1487
        }
1488
        $titleOrig = htmlspecialchars($title);
1489
        $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
1490
        // If title was cropped, offer a tooltip:
1491
        if ($titleOrig != $title) {
1492
            $title = '<span title="' . $titleOrig . '">' . $title . '</span>';
1493
        }
1494
        return $title;
1495
    }
1496
1497
    /**
1498
     * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE.
1499
     *
1500
     * @param bool $prep Wrap result in <em>|</em>
1501
     * @return string Localized [No title] string
1502
     */
1503
    public static function getNoRecordTitle($prep = false)
1504
    {
1505
        $noTitle = '[' .
1506
            htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title'))
1507
            . ']';
1508
        if ($prep) {
1509
            $noTitle = '<em>' . $noTitle . '</em>';
1510
        }
1511
        return $noTitle;
1512
    }
1513
1514
    /**
1515
     * Returns a human readable output of a value from a record
1516
     * For instance a database record relation would be looked up to display the title-value of that record. A checkbox with a "1" value would be "Yes", etc.
1517
     * $table/$col is tablename and fieldname
1518
     * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant)
1519
     *
1520
     * @param string $table Table name, present in TCA
1521
     * @param string $col Field name, present in TCA
1522
     * @param string $value The value of that field from a selected record
1523
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1524
     * @param bool $defaultPassthrough Flag means that values for columns that has no conversion will just be pass through directly (otherwise cropped to 200 chars or returned as "N/A")
1525
     * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown.
1526
     * @param int $uid Uid of the current record
1527
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1528
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1529
     * @throws \InvalidArgumentException
1530
     * @return string|null
1531
     */
1532
    public static function getProcessedValue(
1533
        $table,
1534
        $col,
1535
        $value,
1536
        $fixed_lgd_chars = 0,
1537
        $defaultPassthrough = false,
1538
        $noRecordLookup = false,
1539
        $uid = 0,
1540
        $forceResult = true,
1541
        $pid = 0
1542
    ) {
1543
        if ($col === 'uid') {
1544
            // uid is not in TCA-array
1545
            return $value;
1546
        }
1547
        // Check if table and field is configured
1548
        if (!isset($GLOBALS['TCA'][$table]['columns'][$col]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1549
            return null;
1550
        }
1551
        // Depending on the fields configuration, make a meaningful output value.
1552
        $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? [];
1553
        /*****************
1554
         *HOOK: pre-processing the human readable output from a record
1555
         ****************/
1556
        $referenceObject = new \stdClass();
1557
        $referenceObject->table = $table;
1558
        $referenceObject->fieldName = $col;
1559
        $referenceObject->uid = $uid;
1560
        $referenceObject->value = &$value;
1561
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) {
1562
            GeneralUtility::callUserFunction($_funcRef, $theColConf, $referenceObject);
1563
        }
1564
1565
        $l = '';
1566
        $lang = static::getLanguageService();
1567
        switch ((string)($theColConf['type'] ?? '')) {
1568
            case 'radio':
1569
                $l = self::getLabelFromItemlist($table, $col, $value);
1570
                $l = $lang->sL($l);
1571
                break;
1572
            case 'inline':
1573
                if ($uid) {
1574
                    $finalValues = static::resolveRelationLabels($theColConf, $table, $uid, $value, $noRecordLookup);
1575
                    $l = implode(', ', $finalValues);
1576
                } else {
1577
                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1578
                }
1579
                break;
1580
            case 'select':
1581
            case 'category':
1582
                if (!empty($theColConf['MM'])) {
1583
                    if ($uid) {
1584
                        $finalValues = static::resolveRelationLabels($theColConf, $table, $uid, $value, $noRecordLookup);
1585
                        $l = implode(', ', $finalValues);
1586
                    } else {
1587
                        $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1588
                    }
1589
                } else {
1590
                    $columnTsConfig = [];
1591
                    if ($pid) {
1592
                        $pageTsConfig = self::getPagesTSconfig($pid);
1593
                        if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) {
1594
                            $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'];
1595
                        }
1596
                    }
1597
                    $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig);
1598
                    if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) {
1599
                        if ($noRecordLookup) {
1600
                            $l = $value;
1601
                        } else {
1602
                            $finalValues = [];
1603
                            if ($uid) {
1604
                                $finalValues = static::resolveRelationLabels($theColConf, $table, $uid, $value, $noRecordLookup);
1605
                            }
1606
                            $l = implode(', ', $finalValues);
1607
                        }
1608
                    }
1609
                    if (empty($l) && !empty($value)) {
1610
                        // Use plain database value when label is empty
1611
                        $l = $value;
1612
                    }
1613
                }
1614
                break;
1615
            case 'group':
1616
                if (($theColConf['internal_type'] ?? '') === 'folder') {
1617
                    $l = implode(', ', GeneralUtility::trimExplode(',', $value, true));
1618
                } else {
1619
                    // resolve titles of DB records
1620
                    $finalValues = static::resolveRelationLabels($theColConf, $table, $uid, $value, $noRecordLookup);
1621
                    if ($finalValues !== []) {
1622
                        $l = implode(', ', $finalValues);
1623
                    } else {
1624
                        $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1625
                    }
1626
                }
1627
                break;
1628
            case 'check':
1629
                if (!is_array($theColConf['items'] ?? null)) {
1630
                    $l = $value ? $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes') : $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
1631
                } elseif (count($theColConf['items']) === 1) {
1632
                    reset($theColConf['items']);
1633
                    $invertStateDisplay = current($theColConf['items'])['invertStateDisplay'] ?? false;
1634
                    if ($invertStateDisplay) {
1635
                        $value = !$value;
1636
                    }
1637
                    $l = $value ? $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes') : $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
1638
                } else {
1639
                    $lA = [];
1640
                    foreach ($theColConf['items'] as $key => $val) {
1641
                        if ($value & 2 ** $key) {
1642
                            $lA[] = $lang->sL($val[0]);
1643
                        }
1644
                    }
1645
                    $l = implode(', ', $lA);
1646
                }
1647
                break;
1648
            case 'input':
1649
                // Hide value 0 for dates, but show it for everything else
1650
                // todo: phpstan states that $value always exists and is not nullable. At the moment, this is a false
1651
                //       positive as null can be passed into this method via $value. As soon as more strict types are
1652
                //       used, this isset check must be replaced with a more appropriate check.
1653
                if (isset($value)) {
1654
                    $dateTimeFormats = QueryHelper::getDateTimeFormats();
1655
1656
                    if (GeneralUtility::inList($theColConf['eval'] ?? '', 'date')) {
1657
                        // Handle native date field
1658
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') {
1659
                            $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value);
1660
                        } else {
1661
                            $value = (int)$value;
1662
                        }
1663
                        if (!empty($value)) {
1664
                            $ageSuffix = '';
1665
                            $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
1666
                            $ageDisplayKey = 'disableAgeDisplay';
1667
1668
                            // generate age suffix as long as not explicitly suppressed
1669
                            if (!isset($dateColumnConfiguration[$ageDisplayKey])
1670
                                // non typesafe comparison on intention
1671
                                || $dateColumnConfiguration[$ageDisplayKey] == false
1672
                            ) {
1673
                                $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '')
1674
                                    . self::calcAge(
1675
                                        (int)abs($GLOBALS['EXEC_TIME'] - $value),
1676
                                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
1677
                                    )
1678
                                    . ')';
1679
                            }
1680
1681
                            $l = self::date($value) . $ageSuffix;
1682
                        }
1683
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'time')) {
1684
                        // Handle native time field
1685
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
1686
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value . ' UTC');
1687
                        } else {
1688
                            $value = (int)$value;
1689
                        }
1690
                        if (!empty($value)) {
1691
                            $l = gmdate('H:i', (int)$value);
1692
                        }
1693
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'timesec')) {
1694
                        // Handle native time field
1695
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
1696
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value . ' UTC');
1697
                        } else {
1698
                            $value = (int)$value;
1699
                        }
1700
                        if (!empty($value)) {
1701
                            $l = gmdate('H:i:s', (int)$value);
1702
                        }
1703
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'datetime')) {
1704
                        // Handle native datetime field
1705
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') {
1706
                            $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value);
1707
                        } else {
1708
                            $value = (int)$value;
1709
                        }
1710
                        if (!empty($value)) {
1711
                            $l = self::datetime($value);
1712
                        }
1713
                    } else {
1714
                        $l = $value;
1715
                    }
1716
                }
1717
                break;
1718
            case 'flex':
1719
                $l = strip_tags($value);
1720
                break;
1721
            case 'language':
1722
                $l = $value;
1723
                if ($uid) {
1724
                    $pageId = (int)($table === 'pages' ? $uid : (static::getRecordWSOL($table, (int)$uid, 'pid')['pid'] ?? 0));
1725
                    $languageTitle = GeneralUtility::makeInstance(TranslationConfigurationProvider::class)
1726
                        ->getSystemLanguages($pageId)[(int)$value]['title'] ?? '';
1727
                    if ($languageTitle !== '') {
1728
                        $l = $languageTitle;
1729
                    }
1730
                }
1731
                break;
1732
            default:
1733
                if ($defaultPassthrough) {
1734
                    $l = $value;
1735
                } elseif (isset($theColConf['MM'])) {
1736
                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1737
                } elseif ($value) {
1738
                    $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200);
1739
                }
1740
        }
1741
        // If this field is a password field, then hide the password by changing it to a random number of asterisk (*)
1742
        if (!empty($theColConf['eval']) && stripos($theColConf['eval'], 'password') !== false) {
1743
            $l = '';
1744
            $randomNumber = random_int(5, 12);
1745
            for ($i = 0; $i < $randomNumber; $i++) {
1746
                $l .= '*';
1747
            }
1748
        }
1749
        /*****************
1750
         *HOOK: post-processing the human readable output from a record
1751
         ****************/
1752
        $null = null;
1753
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) {
1754
            $params = [
1755
                'value' => $l,
1756
                'colConf' => $theColConf,
1757
            ];
1758
            $l = GeneralUtility::callUserFunction($_funcRef, $params, $null);
1759
        }
1760
        if ($fixed_lgd_chars && $l) {
1761
            return GeneralUtility::fixed_lgd_cs((string)$l, $fixed_lgd_chars);
1762
        }
1763
        return $l;
1764
    }
1765
1766
    /**
1767
     * Helper method to fetch all labels for all relations of processed Values.
1768
     *
1769
     * @param array $theColConf
1770
     * @param string $table
1771
     * @param string|int|null $recordId
1772
     * @param string|int $value
1773
     * @param bool $noRecordLookup
1774
     * @return array
1775
     */
1776
    protected static function resolveRelationLabels(array $theColConf, string $table, $recordId, $value, bool $noRecordLookup): array
1777
    {
1778
        $finalValues = [];
1779
1780
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1781
        $relationHandler->registerNonTableValues = (bool)($theColConf['allowNonIdValues'] ?? false);
1782
        $relationHandler->start(
1783
            $value,
1784
            $theColConf['allowed'] ?? $theColConf['foreign_table'] ?? '',
1785
            $theColConf['MM'] ?? '',
1786
            $recordId,
1787
            $table,
1788
            $theColConf
1789
        );
1790
1791
        if ($noRecordLookup) {
1792
            $finalValues = array_column($relationHandler->itemArray, 'id');
1793
        } else {
1794
            $relationHandler->getFromDB();
1795
            foreach ($relationHandler->getResolvedItemArray() as $item) {
1796
                $relationRecord = $item['record'];
1797
                static::workspaceOL($item['table'], $relationRecord);
1798
                if (!is_array($relationRecord)) {
1799
                    $finalValues[] = '[' . $item['uid'] . ']';
1800
                } else {
1801
                    $title = static::getRecordTitle($item['table'], $relationRecord);
1802
                    if ($theColConf['foreign_table_prefix'] ?? null) {
1803
                        $title = static::getLanguageService()->sL($theColConf['foreign_table_prefix']) . $title;
1804
                    }
1805
                    $finalValues[] = $title;
1806
                }
1807
            }
1808
        }
1809
1810
        return $finalValues;
1811
    }
1812
1813
    /**
1814
     * Same as ->getProcessedValue() but will go easy on fields like "tstamp" and "pid" which are not configured in TCA - they will be formatted by this function instead.
1815
     *
1816
     * @param string $table Table name, present in TCA
1817
     * @param string $fN Field name
1818
     * @param string $fV Field value
1819
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1820
     * @param int $uid Uid of the current record
1821
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1822
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1823
     * @return string
1824
     * @see getProcessedValue()
1825
     */
1826
    public static function getProcessedValueExtra(
1827
        $table,
1828
        $fN,
1829
        $fV,
1830
        $fixed_lgd_chars = 0,
1831
        $uid = 0,
1832
        $forceResult = true,
1833
        $pid = 0
1834
    ) {
1835
        $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, true, false, $uid, $forceResult, $pid);
1836
        if (!isset($fVnew)) {
1837
            if (is_array($GLOBALS['TCA'][$table])) {
1838
                if ($fN == ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? 0) || $fN == ($GLOBALS['TCA'][$table]['ctrl']['crdate'] ?? 0)) {
1839
                    $fVnew = self::datetime((int)$fV);
1840
                } elseif ($fN === 'pid') {
1841
                    // Fetches the path with no regard to the users permissions to select pages.
1842
                    $fVnew = self::getRecordPath((int)$fV, '1=1', 20);
1843
                } else {
1844
                    $fVnew = $fV;
1845
                }
1846
            }
1847
        }
1848
        return $fVnew;
1849
    }
1850
1851
    /**
1852
     * Returns fields for a table, $table, which would typically be interesting to select
1853
     * This includes uid, the fields defined for title, icon-field.
1854
     * Returned as a list ready for query ($prefix can be set to eg. "pages." if you are selecting from the pages table and want the table name prefixed)
1855
     *
1856
     * @param string $table Table name, present in $GLOBALS['TCA']
1857
     * @param string $prefix Table prefix
1858
     * @param array $fields Preset fields (must include prefix if that is used)
1859
     * @return string List of fields.
1860
     * @internal should only be used from within TYPO3 Core
1861
     */
1862
    public static function getCommonSelectFields($table, $prefix = '', $fields = [])
1863
    {
1864
        $fields[] = $prefix . 'uid';
1865
        if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
1866
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
1867
        }
1868
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) {
1869
            $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
1870
            foreach ($secondFields as $fieldN) {
1871
                $fields[] = $prefix . $fieldN;
1872
            }
1873
        }
1874
        if (static::isTableWorkspaceEnabled($table)) {
1875
            $fields[] = $prefix . 't3ver_state';
1876
            $fields[] = $prefix . 't3ver_wsid';
1877
        }
1878
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) {
1879
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
1880
        }
1881
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
1882
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
1883
        }
1884
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
1885
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
1886
        }
1887
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) {
1888
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
1889
        }
1890
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) {
1891
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
1892
        }
1893
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
1894
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
1895
        }
1896
        return implode(',', array_unique($fields));
1897
    }
1898
1899
    /*******************************************
1900
     *
1901
     * Backend Modules API functions
1902
     *
1903
     *******************************************/
1904
1905
    /**
1906
     * Returns CSH help text (description), if configured for, as an array (title, description)
1907
     *
1908
     * @param string $table Table name
1909
     * @param string $field Field name
1910
     * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
1911
     * @internal should only be used from within TYPO3 Core
1912
     */
1913
    public static function helpTextArray($table, $field)
1914
    {
1915
        if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
1916
            static::getLanguageService()->loadSingleTableDescription($table);
1917
        }
1918
        $output = [
1919
            'description' => null,
1920
            'title' => null,
1921
            'moreInfo' => false,
1922
        ];
1923
        if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
1924
            $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
1925
            // Add alternative title, if defined
1926
            if ($data['alttitle'] ?? false) {
1927
                $output['title'] = $data['alttitle'];
1928
            }
1929
            // If we have more information to show and access to the cshmanual
1930
            // This is effectively a long list of ORs, but also allows for any to be unset. The first one set and truthy
1931
            // will evaluate the whole chain to true.
1932
            if (($data['image_descr'] ?? $data['seeAlso'] ?? $data['details'] ?? $data['syntax'] ?? false)
1933
                && static::getBackendUserAuthentication()->check('modules', 'help_cshmanual')
1934
            ) {
1935
                $output['moreInfo'] = true;
1936
            }
1937
            // Add description
1938
            if ($data['description'] ?? null) {
1939
                $output['description'] = $data['description'];
1940
            }
1941
        }
1942
        return $output;
1943
    }
1944
1945
    /**
1946
     * Returns CSH help text
1947
     *
1948
     * @param string $table Table name
1949
     * @param string $field Field name
1950
     * @return string HTML content for help text
1951
     * @see cshItem()
1952
     * @internal should only be used from within TYPO3 Core
1953
     */
1954
    public static function helpText($table, $field)
1955
    {
1956
        $helpTextArray = self::helpTextArray($table, $field);
1957
        $output = '';
1958
        $arrow = '';
1959
        // Put header before the rest of the text
1960
        if ($helpTextArray['title'] !== null) {
1961
            $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
1962
        }
1963
        // Add see also arrow if we have more info
1964
        if ($helpTextArray['moreInfo']) {
1965
            /** @var IconFactory $iconFactory */
1966
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1967
            $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
1968
        }
1969
        // Wrap description and arrow in p tag
1970
        if ($helpTextArray['description'] !== null || $arrow) {
1971
            $output .= '<p class="help-short">' . nl2br(htmlspecialchars((string)$helpTextArray['description'])) . $arrow . '</p>';
1972
        }
1973
        return $output;
1974
    }
1975
1976
    /**
1977
     * API function that wraps the text / html in help text, so if a user hovers over it
1978
     * the help text will show up
1979
     *
1980
     * @param string $table The table name for which the help should be shown
1981
     * @param string $field The field name for which the help should be shown
1982
     * @param string $text The text which should be wrapped with the help text
1983
     * @param array $overloadHelpText Array with text to overload help text
1984
     * @return string the HTML code ready to render
1985
     * @internal should only be used from within TYPO3 Core
1986
     */
1987
    public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
1988
    {
1989
        // Initialize some variables
1990
        $helpText = '';
1991
        $abbrClassAdd = '';
1992
        $hasHelpTextOverload = !empty($overloadHelpText);
1993
        // Get the help text that should be shown on hover
1994
        if (!$hasHelpTextOverload) {
1995
            $helpText = self::helpText($table, $field);
1996
        }
1997
        // If there's a help text or some overload information, proceed with preparing an output
1998
        if (!empty($helpText) || $hasHelpTextOverload) {
1999
            // If no text was given, just use the regular help icon
2000
            if ($text == '') {
2001
                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2002
                $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2003
                $abbrClassAdd = ' help-teaser-icon';
2004
            }
2005
            $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2006
            $wrappedText = '<span class="help-link" data-table="' . $table . '" data-field="' . $field . '" data-bs-content="<p></p>"';
2007
            // The overload array may provide a title and a description
2008
            // If either one is defined, add them to the "data" attributes
2009
            if ($hasHelpTextOverload) {
2010
                if (isset($overloadHelpText['title'])) {
2011
                    $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2012
                }
2013
                if (isset($overloadHelpText['description'])) {
2014
                    $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2015
                }
2016
            }
2017
            $wrappedText .= '>' . $text . '</span>';
2018
            return $wrappedText;
2019
        }
2020
        return $text;
2021
    }
2022
2023
    /**
2024
     * API for getting CSH icons/text for use in backend modules.
2025
     * TCA_DESCR will be loaded if it isn't already
2026
     *
2027
     * @param string $table Table name ('_MOD_'+module name)
2028
     * @param string $field Field name (CSH locallang main key)
2029
     * @param string $_ (unused)
2030
     * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2031
     * @return string HTML content for help text
2032
     */
2033
    public static function cshItem($table, $field, $_ = '', $wrap = '')
2034
    {
2035
        static::getLanguageService()->loadSingleTableDescription($table);
2036
        if (is_array($GLOBALS['TCA_DESCR'][$table] ?? null)
2037
            && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field] ?? null)
2038
        ) {
2039
            // Creating short description
2040
            $output = self::wrapInHelp($table, $field);
2041
            if ($output && $wrap) {
2042
                $wrParts = explode('|', $wrap);
2043
                $output = $wrParts[0] . $output . $wrParts[1];
2044
            }
2045
            return $output;
2046
        }
2047
        return '';
2048
    }
2049
2050
    /**
2051
     * Returns the preview url
2052
     *
2053
     * It will detect the correct domain name if needed and provide the link with the right back path.
2054
     *
2055
     * @param int $pageUid Page UID
2056
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2057
     * @param array|null $rootLine If root line is supplied the function will look for the first found domain record and use that URL instead (if found)
2058
     * @param string $anchorSection Optional anchor to the URL
2059
     * @param string $alternativeUrl An alternative URL that, if set, will ignore other parameters except $switchFocus: It will return the window.open command wrapped around this URL!
2060
     * @param string $additionalGetVars Additional GET variables.
2061
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2062
     * @return string
2063
     */
2064
    public static function getPreviewUrl(
2065
        $pageUid,
2066
        $backPath = '',
2067
        $rootLine = null,
2068
        $anchorSection = '',
2069
        $alternativeUrl = '',
2070
        $additionalGetVars = '',
2071
        &$switchFocus = true
2072
    ): string {
2073
        $viewScript = '/index.php?id=';
2074
        if ($alternativeUrl) {
2075
            $viewScript = $alternativeUrl;
2076
        }
2077
2078
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2079
            $hookObj = GeneralUtility::makeInstance($className);
2080
            if (method_exists($hookObj, 'preProcess')) {
2081
                $hookObj->preProcess(
2082
                    $pageUid,
2083
                    $backPath,
2084
                    $rootLine,
2085
                    $anchorSection,
2086
                    $viewScript,
2087
                    $additionalGetVars,
2088
                    $switchFocus
2089
                );
2090
            }
2091
        }
2092
2093
        // If there is an alternative URL or the URL has been modified by a hook, use that one.
2094
        if ($alternativeUrl || $viewScript !== '/index.php?id=') {
2095
            $previewUrl = $viewScript;
2096
        } else {
2097
            $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2098
            $pageInfo = self::readPageAccess($pageUid, $permissionClause) ?: [];
2099
            // prepare custom context for link generation (to allow for example time based previews)
2100
            $context = clone GeneralUtility::makeInstance(Context::class);
2101
            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context);
2102
2103
            // Build the URL with a site as prefix, if configured
2104
            $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
2105
            // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is
2106
            $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid);
2107
            try {
2108
                $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine);
2109
            } catch (SiteNotFoundException $e) {
2110
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794919);
2111
            }
2112
            // Create a multi-dimensional array out of the additional get vars
2113
            $additionalQueryParams = [];
2114
            parse_str($additionalGetVars, $additionalQueryParams);
2115
            if (isset($additionalQueryParams['L'])) {
2116
                $additionalQueryParams['_language'] = $additionalQueryParams['_language'] ?? $additionalQueryParams['L'];
2117
                unset($additionalQueryParams['L']);
2118
            }
2119
            try {
2120
                $previewUrl = (string)$site->getRouter($context)->generateUri(
2121
                    $pageUid,
2122
                    $additionalQueryParams,
2123
                    $anchorSection,
2124
                    RouterInterface::ABSOLUTE_URL
2125
                );
2126
            } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
2127
                throw new UnableToLinkToPageException(sprintf('The link to the page with ID "%d" could not be generated: %s', $pageUid, $e->getMessage()), 1559794914, $e);
2128
            }
2129
        }
2130
2131
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2132
            $hookObj = GeneralUtility::makeInstance($className);
2133
            if (method_exists($hookObj, 'postProcess')) {
2134
                $previewUrl = $hookObj->postProcess(
2135
                    $previewUrl,
2136
                    $pageUid,
2137
                    $rootLine,
2138
                    $anchorSection,
2139
                    $viewScript,
2140
                    $additionalGetVars,
2141
                    $switchFocus
2142
                );
2143
            }
2144
        }
2145
2146
        return $previewUrl;
2147
    }
2148
2149
    /**
2150
     * Makes click menu link (context sensitive menu)
2151
     *
2152
     * Returns $str wrapped in a link which will activate the context sensitive
2153
     * menu for the record ($table/$uid) or file ($table = file)
2154
     * The link will load the top frame with the parameter "&item" which is the table, uid
2155
     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2156
     *
2157
     * @param string $content String to be wrapped in link, typ. image tag.
2158
     * @param string $table Table name/File path. If the icon is for a database
2159
     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2160
     * the absolute filepath
2161
     * @param int|string $uid If icon is for database record this is the UID for the
2162
     * record from $table or identifier for sys_file record
2163
     * @param string $context Set tree if menu is called from tree view
2164
     * @param string $_addParams NOT IN USE Deprecated since TYPO3 11, will be removed in TYPO3 12.
2165
     * @param string $_enDisItems NOT IN USE Deprecated since TYPO3 11, will be removed in TYPO3 12.
2166
     * @param bool $returnTagParameters If set, will return only the onclick
2167
     * JavaScript, not the whole link. Deprecated since TYPO3 11, will be removed in TYPO3 12.
2168
     *
2169
     * @return string|array The link wrapped input string.
2170
     */
2171
    public static function wrapClickMenuOnIcon(
2172
        $content,
2173
        $table,
2174
        $uid = 0,
2175
        $context = '',
2176
        $_addParams = '',
2177
        $_enDisItems = '',
2178
        $returnTagParameters = false
2179
    ) {
2180
        $tagParameters = self::getClickMenuOnIconTagParameters((string)$table, $uid, (string)$context);
2181
2182
        if ($_addParams !== '') {
2183
            trigger_error('Calling BackendUtility::wrapClickMenuOnIcon() with unused 5th parameter is deprecated and will be removed in v12.', E_USER_DEPRECATED);
2184
        }
2185
        if ($_enDisItems !== '') {
2186
            trigger_error('Calling BackendUtility::wrapClickMenuOnIcon() with unused 6th parameter is deprecated and will be removed in v12.', E_USER_DEPRECATED);
2187
        }
2188
        if ($returnTagParameters) {
2189
            trigger_error('Calling BackendUtility::wrapClickMenuOnIcon() with 7th parameter set to true is deprecated and will be removed in v12. Please use BackendUtility::getClickMenuOnIconTagParameters() instead.', E_USER_DEPRECATED);
2190
            return $tagParameters;
2191
        }
2192
        return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2193
    }
2194
2195
    /**
2196
     * @param string $table Table name/File path. If the icon is for a database
2197
     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2198
     * the absolute filepath
2199
     * @param int|string $uid If icon is for database record this is the UID for the
2200
     * record from $table or identifier for sys_file record
2201
     * @param string $context Set tree if menu is called from tree view
2202
     * @return array
2203
     */
2204
    public static function getClickMenuOnIconTagParameters(string $table, $uid = 0, string $context = ''): array
2205
    {
2206
        return [
2207
            'class' => 't3js-contextmenutrigger',
2208
            'data-table' => $table,
2209
            'data-uid' => (string)$uid,
2210
            'data-context' => $context,
2211
        ];
2212
    }
2213
2214
    /**
2215
     * Returns a URL with a command to TYPO3 Datahandler
2216
     *
2217
     * @param string $parameters Set of GET params to send. Example: "&cmd[tt_content][123][move]=456" or "&data[tt_content][123][hidden]=1&data[tt_content][123][title]=Hello%20World
2218
     * @param string $redirectUrl Redirect URL, default is to use $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
2219
     * @return string
2220
     */
2221
    public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2222
    {
2223
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2224
        $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2225
        $url .= rawurlencode($redirectUrl ?: $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri());
2226
        return $url;
2227
    }
2228
2229
    /**
2230
     * Returns a selector box "function menu" for a module
2231
     * See Inside TYPO3 for details about how to use / make Function menus
2232
     *
2233
     * @param mixed $mainParams The "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
2234
     * @param string $elementName The form elements name, probably something like "SET[...]
2235
     * @param string $currentValue The value to be selected currently.
2236
     * @param array $menuItems An array with the menu items for the selector box
2237
     * @param string $script The script to send the &id to, if empty it's automatically found
2238
     * @param string $addParams Additional parameters to pass to the script.
2239
     * @return string HTML code for selector box
2240
     */
2241
    public static function getFuncMenu(
2242
        $mainParams,
2243
        $elementName,
2244
        $currentValue,
2245
        $menuItems,
2246
        $script = '',
2247
        $addParams = ''
2248
    ): string {
2249
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2250
            return '';
2251
        }
2252
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2253
        $options = [];
2254
        foreach ($menuItems as $value => $label) {
2255
            $options[] = '<option value="'
2256
                . htmlspecialchars($value) . '"'
2257
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2258
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2259
        }
2260
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2261
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2262
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2263
        if (!empty($options)) {
2264
            // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2265
            $attributes = GeneralUtility::implodeAttributes([
2266
                'name' => $elementName,
2267
                'class' => 'form-select mb-3',
2268
                'data-menu-identifier' => $dataMenuIdentifier,
2269
                'data-global-event' => 'change',
2270
                'data-action-navigate' => '$data=~s/$value/',
2271
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2272
            ], true);
2273
            return sprintf(
2274
                '<select %s>%s</select>',
2275
                $attributes,
2276
                implode('', $options)
2277
            );
2278
        }
2279
        return '';
2280
    }
2281
2282
    /**
2283
     * Returns a selector box to switch the view
2284
     * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose.
2285
     * Mingling with getFuncMenu would harm the docHeader Menu.
2286
     *
2287
     * @param mixed $mainParams The "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
2288
     * @param string $elementName The form elements name, probably something like "SET[...]
2289
     * @param string $currentValue The value to be selected currently.
2290
     * @param array $menuItems An array with the menu items for the selector box
2291
     * @param string $script The script to send the &id to, if empty it's automatically found
2292
     * @param string $addParams Additional parameters to pass to the script.
2293
     * @return string HTML code for selector box
2294
     */
2295
    public static function getDropdownMenu(
2296
        $mainParams,
2297
        $elementName,
2298
        $currentValue,
2299
        $menuItems,
2300
        $script = '',
2301
        $addParams = ''
2302
    ) {
2303
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2304
            return '';
2305
        }
2306
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2307
        $options = [];
2308
        foreach ($menuItems as $value => $label) {
2309
            $options[] = '<option value="'
2310
                . htmlspecialchars($value) . '"'
2311
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2312
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2313
        }
2314
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2315
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2316
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2317
        if (!empty($options)) {
2318
            // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2319
            $attributes = GeneralUtility::implodeAttributes([
2320
                'name' => $elementName,
2321
                'data-menu-identifier' => $dataMenuIdentifier,
2322
                'data-global-event' => 'change',
2323
                'data-action-navigate' => '$data=~s/$value/',
2324
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2325
            ], true);
2326
            return '
2327
			<div class="input-group">
2328
				<!-- Function Menu of module -->
2329
				<select class="form-select" ' . $attributes . '>
2330
					' . implode(LF, $options) . '
2331
				</select>
2332
			</div>
2333
						';
2334
        }
2335
        return '';
2336
    }
2337
2338
    /**
2339
     * Checkbox function menu.
2340
     * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox.
2341
     *
2342
     * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
2343
     * @param string $elementName The form elements name, probably something like "SET[...]
2344
     * @param string|bool $currentValue The value to be selected currently.
2345
     * @param string $script The script to send the &id to, if empty it's automatically found
2346
     * @param string $addParams Additional parameters to pass to the script.
2347
     * @param string $tagParams Additional attributes for the checkbox input tag
2348
     * @return string HTML code for checkbox
2349
     * @see getFuncMenu()
2350
     */
2351
    public static function getFuncCheck(
2352
        $mainParams,
2353
        $elementName,
2354
        $currentValue,
2355
        $script = '',
2356
        $addParams = '',
2357
        $tagParams = ''
2358
    ) {
2359
        // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2360
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2361
        $attributes = GeneralUtility::implodeAttributes([
2362
            'type' => 'checkbox',
2363
            'class' => 'form-check-input',
2364
            'name' => $elementName,
2365
            'value' => '1',
2366
            'data-global-event' => 'change',
2367
            'data-action-navigate' => '$data=~s/$value/',
2368
            'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName),
2369
            'data-empty-value' => '0',
2370
        ], true);
2371
        return
2372
            '<input ' . $attributes .
2373
            ($currentValue ? ' checked="checked"' : '') .
2374
            ($tagParams ? ' ' . $tagParams : '') .
2375
            ' />';
2376
    }
2377
2378
    /**
2379
     * Input field function menu
2380
     * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange"
2381
     *
2382
     * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
2383
     * @param string $elementName The form elements name, probably something like "SET[...]
2384
     * @param string $currentValue The value to be selected currently.
2385
     * @param int $size Relative size of input field, max is 48
2386
     * @param string $script The script to send the &id to, if empty it's automatically found
2387
     * @param string $addParams Additional parameters to pass to the script.
2388
     * @return string HTML code for input text field.
2389
     * @see getFuncMenu()
2390
     * @todo not used at least since TYPO3 v9, drop in TYPO3 v12.0
2391
     */
2392
    public static function getFuncInput(
2393
        $mainParams,
2394
        $elementName,
2395
        $currentValue,
2396
        $size = 10,
2397
        $script = '',
2398
        $addParams = ''
2399
    ) {
2400
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2401
        $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value);';
2402
        return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />';
2403
    }
2404
2405
    /**
2406
     * Builds the URL to the current script with given arguments
2407
     *
2408
     * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
2409
     * @param string $addParams Additional parameters to pass to the script.
2410
     * @param string $script The script to send the &id to, if empty it's automatically found
2411
     * @return string The complete script URL
2412
     * @todo Check if this can be removed or replaced by routing
2413
     */
2414
    protected static function buildScriptUrl($mainParams, $addParams, $script = '')
2415
    {
2416
        if (!is_array($mainParams)) {
2417
            $mainParams = ['id' => $mainParams];
2418
        }
2419
2420
        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
2421
            && ($route = $GLOBALS['TYPO3_REQUEST']->getAttribute('route')) instanceof Route
2422
        ) {
2423
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2424
            $scriptUrl = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'), $mainParams);
2425
            $scriptUrl .= $addParams;
2426
        } else {
2427
            if (!$script) {
2428
                $script = PathUtility::basename(Environment::getCurrentScript());
2429
            }
2430
            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
2431
        }
2432
2433
        return $scriptUrl;
2434
    }
2435
2436
    /**
2437
     * Call to update the page tree frame (or something else..?) after
2438
     * use 'updatePageTree' as a first parameter will set the page tree to be updated.
2439
     *
2440
     * @param string $set Key to set the update signal. When setting, this value contains strings telling WHAT to set. At this point it seems that the value "updatePageTree" is the only one it makes sense to set. If empty, all update signals will be removed.
2441
     * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
2442
     * @see BackendUtility::getUpdateSignalDetails()
2443
     */
2444
    public static function setUpdateSignal($set = '', $params = '')
2445
    {
2446
        $beUser = static::getBackendUserAuthentication();
2447
        $modData = $beUser->getModuleData(
2448
            BackendUtility::class . '::getUpdateSignal',
2449
            'ses'
2450
        );
2451
        if ($set) {
2452
            $modData[$set] = [
2453
                'set' => $set,
2454
                'parameter' => $params,
2455
            ];
2456
        } else {
2457
            // clear the module data
2458
            $modData = [];
2459
        }
2460
        $beUser->pushModuleData(BackendUtility::class . '::getUpdateSignal', $modData);
2461
    }
2462
2463
    /**
2464
     * Call to update the page tree frame (or something else..?) if this is set by the function
2465
     * setUpdateSignal(). It will return some JavaScript that does the update
2466
     *
2467
     * @return string HTML javascript code
2468
     * @see BackendUtility::setUpdateSignal()
2469
     * @internal use getUpdateSignalDetails() instead, will be deprecated in TYPO3 v12.0
2470
     */
2471
    public static function getUpdateSignalCode()
2472
    {
2473
        $signals = [];
2474
        $modData = static::getBackendUserAuthentication()->getModuleData(
2475
            BackendUtility::class . '::getUpdateSignal',
2476
            'ses'
2477
        );
2478
        if (empty($modData)) {
2479
            return '';
2480
        }
2481
        // Hook: Allows to let TYPO3 execute your JS code
2482
        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
2483
        // Loop through all setUpdateSignals and get the JS code
2484
        foreach ($modData as $set => $val) {
2485
            if (isset($updateSignals[$set])) {
2486
                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => ''];
2487
                $ref = null;
2488
                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
2489
                $signals[] = $params['JScode'];
2490
            } else {
2491
                switch ($set) {
2492
                    case 'updatePageTree':
2493
                        $signals[] = '
2494
								if (top) {
2495
									top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"));
2496
								}';
2497
                        break;
2498
                    case 'updateFolderTree':
2499
                        $signals[] = '
2500
								if (top) {
2501
									top.document.dispatchEvent(new CustomEvent("typo3:filestoragetree:refresh"));
2502
								}';
2503
                        break;
2504
                    case 'updateModuleMenu':
2505
                        $signals[] = '
2506
								if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) {
2507
									top.TYPO3.ModuleMenu.App.refreshMenu();
2508
								}';
2509
                        break;
2510
                    case 'updateTopbar':
2511
                        $signals[] = '
2512
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) {
2513
									top.TYPO3.Backend.Topbar.refresh();
2514
								}';
2515
                        break;
2516
                }
2517
            }
2518
        }
2519
        $content = implode(LF, $signals);
2520
        // For backwards compatibility, should be replaced
2521
        self::setUpdateSignal();
2522
        return $content;
2523
    }
2524
2525
    /**
2526
     * Gets instructions for update signals (e.g. page tree shall be refreshed,
2527
     * since some page title has been modified during the current HTTP request).
2528
     *
2529
     * @return array{html: list<string>, script: list<string>}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{html: list<string>, script: list<string>} at position 4 could not be parsed: Expected '}' at position 4, but found 'list'.
Loading history...
2530
     * @see BackendUtility::setUpdateSignal()
2531
     */
2532
    public static function getUpdateSignalDetails(): array
2533
    {
2534
        $details = [
2535
            'html' => [],
2536
            // @todo deprecate inline JavaScript in TYPO3 v12.0
2537
            'script' => [],
2538
        ];
2539
        $modData = static::getBackendUserAuthentication()->getModuleData(
2540
            BackendUtility::class . '::getUpdateSignal',
2541
            'ses'
2542
        );
2543
        if (empty($modData)) {
2544
            return $details;
2545
        }
2546
        // Hook: Allows to let TYPO3 execute your JS code
2547
        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
2548
        // Loop through all setUpdateSignals and get the JS code
2549
        foreach ($modData as $set => $val) {
2550
            if (isset($updateSignals[$set])) {
2551
                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => '', 'html' => ''];
2552
                $ref = null;
2553
                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
2554
                // @todo verify and adjust documentation
2555
                if (!empty($params['html'])) {
2556
                    $details['html'][] = $params['html'];
2557
                } elseif (!empty($params['JScode'])) {
2558
                    // @todo deprecate in TYPO3 v12.0, avoid inline JavaScript
2559
                    $details['script'][] = $params['JScode'];
2560
                }
2561
            } else {
2562
                switch ($set) {
2563
                    case 'updatePageTree':
2564
                        $details['html'][] = ImmediateActionElement::dispatchCustomEvent(
2565
                            'typo3:pagetree:refresh',
2566
                            null,
2567
                            true
2568
                        );
2569
                        break;
2570
                    case 'updateFolderTree':
2571
                        $details['html'][] = ImmediateActionElement::dispatchCustomEvent(
2572
                            'typo3:filestoragetree:refresh',
2573
                            null,
2574
                            true
2575
                        );
2576
                        break;
2577
                    case 'updateModuleMenu':
2578
                        $details['html'][] = ImmediateActionElement::forAction(
2579
                            'TYPO3.ModuleMenu.App.refreshMenu',
2580
                        );
2581
                        break;
2582
                    case 'updateTopbar':
2583
                        $details['html'][] = ImmediateActionElement::forAction(
2584
                            'TYPO3.Backend.Topbar.refresh'
2585
                        );
2586
                        break;
2587
                }
2588
            }
2589
        }
2590
        // reset update signals
2591
        self::setUpdateSignal();
2592
        return $details;
2593
    }
2594
2595
    /**
2596
     * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
2597
     * This is kind of session variable management framework for the backend users.
2598
     * If a key from MOD_MENU is set in the CHANGED_SETTINGS array (eg. a value is passed to the script from the outside), this value is put into the settings-array
2599
     * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules.
2600
     *
2601
     * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus.
2602
     * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus.
2603
     * @param string $modName modName is the name of this module. Used to get the correct module data.
2604
     * @param string $type If type is 'ses' then the data is stored as session-lasting data. This means that it'll be wiped out the next time the user logs in.
2605
     * @param string $dontValidateList dontValidateList can be used to list variables that should not be checked if their value is found in the MOD_MENU array. Used for dynamically generated menus.
2606
     * @param string $setDefaultList List of default values from $MOD_MENU to set in the output array (only if the values from MOD_MENU are not arrays)
2607
     * @throws \RuntimeException
2608
     * @return array The array $settings, which holds a key for each MOD_MENU key and the values of each key will be within the range of values for each menuitem
2609
     */
2610
    public static function getModuleData(
2611
        $MOD_MENU,
2612
        $CHANGED_SETTINGS,
2613
        $modName,
2614
        $type = '',
2615
        $dontValidateList = '',
2616
        $setDefaultList = ''
2617
    ) {
2618
        if ($modName && is_string($modName)) {
2619
            // Getting stored user-data from this module:
2620
            $beUser = static::getBackendUserAuthentication();
2621
            $settings = $beUser->getModuleData($modName, $type);
2622
            $changed = 0;
2623
            if (!is_array($settings)) {
2624
                $changed = 1;
2625
                $settings = [
2626
                    'function' => null,
2627
                    'language' => null,
2628
                    'constant_editor_cat' => null,
2629
                ];
2630
            }
2631
            if (is_array($MOD_MENU)) {
0 ignored issues
show
introduced by
The condition is_array($MOD_MENU) is always true.
Loading history...
2632
                foreach ($MOD_MENU as $key => $var) {
2633
                    // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array.
2634
                    if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) {
2635
                        if (is_array($CHANGED_SETTINGS[$key])) {
2636
                            $serializedSettings = serialize($CHANGED_SETTINGS[$key]);
2637
                            if ((string)$settings[$key] !== $serializedSettings) {
2638
                                $settings[$key] = $serializedSettings;
2639
                                $changed = 1;
2640
                            }
2641
                        } else {
2642
                            if ((string)($settings[$key] ?? '') !== (string)($CHANGED_SETTINGS[$key] ?? '')) {
2643
                                $settings[$key] = $CHANGED_SETTINGS[$key];
2644
                                $changed = 1;
2645
                            }
2646
                        }
2647
                    }
2648
                    // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted
2649
                    if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) {
2650
                        // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted.
2651
                        if (is_array($settings[$key] ?? null) || !isset($MOD_MENU[$key][$settings[$key] ?? null])) {
2652
                            $settings[$key] = (string)key($var);
2653
                            $changed = 1;
2654
                        }
2655
                    }
2656
                    // Sets default values (only strings/checkboxes, not menus)
2657
                    if ($setDefaultList && !is_array($var)) {
2658
                        if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) {
2659
                            $settings[$key] = (string)$var;
2660
                        }
2661
                    }
2662
                }
2663
            } else {
2664
                throw new \RuntimeException('No menu', 1568119229);
2665
            }
2666
            if ($changed) {
2667
                $beUser->pushModuleData($modName, $settings);
2668
            }
2669
            return $settings;
2670
        }
2671
        throw new \RuntimeException('Wrong module name "' . $modName . '"', 1568119221);
2672
    }
2673
2674
    /*******************************************
2675
     *
2676
     * Core
2677
     *
2678
     *******************************************/
2679
    /**
2680
     * Unlock or Lock a record from $table with $uid
2681
     * If $table and $uid is not set, then all locking for the current BE_USER is removed!
2682
     *
2683
     * @param string $table Table name
2684
     * @param int $uid Record uid
2685
     * @param int $pid Record pid
2686
     * @internal
2687
     */
2688
    public static function lockRecords($table = '', $uid = 0, $pid = 0)
2689
    {
2690
        $beUser = static::getBackendUserAuthentication();
2691
        if (isset($beUser->user['uid'])) {
2692
            $userId = (int)$beUser->user['uid'];
2693
            if ($table && $uid) {
2694
                $fieldsValues = [
2695
                    'userid' => $userId,
2696
                    'feuserid' => 0,
2697
                    'tstamp' => $GLOBALS['EXEC_TIME'],
2698
                    'record_table' => $table,
2699
                    'record_uid' => $uid,
2700
                    'username' => $beUser->user['username'],
2701
                    'record_pid' => $pid,
2702
                ];
2703
                GeneralUtility::makeInstance(ConnectionPool::class)
2704
                    ->getConnectionForTable('sys_lockedrecords')
2705
                    ->insert(
2706
                        'sys_lockedrecords',
2707
                        $fieldsValues
2708
                    );
2709
            } else {
2710
                GeneralUtility::makeInstance(ConnectionPool::class)
2711
                    ->getConnectionForTable('sys_lockedrecords')
2712
                    ->delete(
2713
                        'sys_lockedrecords',
2714
                        ['userid' => (int)$userId]
2715
                    );
2716
            }
2717
        }
2718
    }
2719
2720
    /**
2721
     * Returns information about whether the record from table, $table, with uid, $uid is currently locked
2722
     * (edited by another user - which should issue a warning).
2723
     * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts
2724
     * are activated - which means that a user CAN have a record "open" without having it locked.
2725
     * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient.
2726
     *
2727
     * @param string $table Table name
2728
     * @param int $uid Record uid
2729
     * @return array|bool
2730
     * @internal
2731
     */
2732
    public static function isRecordLocked($table, $uid)
2733
    {
2734
        $runtimeCache = self::getRuntimeCache();
2735
        $cacheId = 'backend-recordLocked';
2736
        $recordLockedCache = $runtimeCache->get($cacheId);
2737
        if ($recordLockedCache !== false) {
2738
            $lockedRecords = $recordLockedCache;
2739
        } else {
2740
            $lockedRecords = [];
2741
2742
            $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords');
2743
            $result = $queryBuilder
2744
                ->select('*')
2745
                ->from('sys_lockedrecords')
2746
                ->where(
2747
                    $queryBuilder->expr()->neq(
2748
                        'sys_lockedrecords.userid',
2749
                        $queryBuilder->createNamedParameter(
2750
                            static::getBackendUserAuthentication()->user['uid'],
2751
                            \PDO::PARAM_INT
2752
                        )
2753
                    ),
2754
                    $queryBuilder->expr()->gt(
2755
                        'sys_lockedrecords.tstamp',
2756
                        $queryBuilder->createNamedParameter(
2757
                            $GLOBALS['EXEC_TIME'] - 2 * 3600,
2758
                            \PDO::PARAM_INT
2759
                        )
2760
                    )
2761
                )
2762
                ->execute();
2763
2764
            $lang = static::getLanguageService();
2765
            while ($row = $result->fetchAssociative()) {
2766
                $row += [
2767
                    'userid' => 0,
2768
                    'record_pid' => 0,
2769
                    'feuserid' => 0,
2770
                    'username' => '',
2771
                    'record_table' => '',
2772
                    'record_uid' => 0,
2773
2774
                ];
2775
                // Get the type of the user that locked this record:
2776
                if ($row['userid']) {
2777
                    $userTypeLabel = 'beUser';
2778
                } elseif ($row['feuserid']) {
2779
                    $userTypeLabel = 'feUser';
2780
                } else {
2781
                    $userTypeLabel = 'user';
2782
                }
2783
                $userType = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel);
2784
                // Get the username (if available):
2785
                $userName = ($row['username'] ?? '') ?: $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.unknownUser');
2786
2787
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row;
2788
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf(
2789
                    $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'),
2790
                    $userType,
2791
                    $userName,
2792
                    self::calcAge(
2793
                        $GLOBALS['EXEC_TIME'] - $row['tstamp'],
2794
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2795
                    )
2796
                );
2797
                if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) {
2798
                    $lockedRecords['pages:' . ($row['record_pid'] ?? '')]['msg'] = sprintf(
2799
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'),
2800
                        $userType,
2801
                        $userName,
2802
                        self::calcAge(
2803
                            $GLOBALS['EXEC_TIME'] - $row['tstamp'],
2804
                            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2805
                        )
2806
                    );
2807
                }
2808
            }
2809
            $runtimeCache->set($cacheId, $lockedRecords);
2810
        }
2811
2812
        return $lockedRecords[$table . ':' . $uid] ?? false;
2813
    }
2814
2815
    /**
2816
     * Returns TSConfig for the TCEFORM object in Page TSconfig.
2817
     * Used in TCEFORMs
2818
     *
2819
     * @param string $table Table name present in TCA
2820
     * @param array $row Row from table
2821
     * @return array
2822
     */
2823
    public static function getTCEFORM_TSconfig($table, $row)
2824
    {
2825
        $res = [];
2826
        // Get main config for the table
2827
        [$TScID, $cPid] = self::getTSCpid($table, $row['uid'] ?? 0, $row['pid'] ?? 0);
2828
        if ($TScID >= 0) {
2829
            $tsConfig = static::getPagesTSconfig($TScID)['TCEFORM.'][$table . '.'] ?? [];
2830
            $typeVal = self::getTCAtypeValue($table, $row);
2831
            foreach ($tsConfig as $key => $val) {
2832
                if (is_array($val)) {
2833
                    $fieldN = substr($key, 0, -1);
2834
                    $res[$fieldN] = $val;
2835
                    unset($res[$fieldN]['types.']);
2836
                    if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'] ?? false)) {
2837
                        ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']);
2838
                    }
2839
                }
2840
            }
2841
        }
2842
        $res['_CURRENT_PID'] = $cPid;
2843
        $res['_THIS_UID'] = $row['uid'] ?? 0;
2844
        // So the row will be passed to foreign_table_where_query()
2845
        $res['_THIS_ROW'] = $row;
2846
        return $res;
2847
    }
2848
2849
    /**
2850
     * Find the real PID of the record (with $uid from $table).
2851
     * This MAY be impossible if the pid is set as a reference to the former record or a page (if two records are created at one time).
2852
     *
2853
     * @param string $table Table name
2854
     * @param int $uid Record uid
2855
     * @param int|string $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return
2856
     * @return int|null
2857
     * @internal
2858
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord()
2859
     * @see \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid()
2860
     */
2861
    public static function getTSconfig_pidValue($table, $uid, $pid)
2862
    {
2863
        // If pid is an integer this takes precedence in our lookup.
2864
        if (MathUtility::canBeInterpretedAsInteger($pid)) {
2865
            $thePidValue = (int)$pid;
2866
            // If ref to another record, look that record up.
2867
            if ($thePidValue < 0) {
2868
                $pidRec = self::getRecord($table, abs($thePidValue), 'pid');
2869
                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
2870
            }
2871
        } else {
2872
            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
2873
            $rr = self::getRecord($table, $uid);
2874
            $thePidValue = null;
2875
            if (is_array($rr)) {
2876
                // First check if the t3ver_oid value is greater 0, which means
2877
                // it is a workspace element. If so, get the "real" record:
2878
                if ((int)($rr['t3ver_oid'] ?? 0) > 0) {
2879
                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
2880
                    if (is_array($rr)) {
2881
                        $thePidValue = $rr['pid'];
2882
                    }
2883
                } else {
2884
                    // Returning the "pid" of the record
2885
                    $thePidValue = $rr['pid'];
2886
                }
2887
            }
2888
            if (!$thePidValue) {
2889
                // Returns -1 if the record with this pid was not found.
2890
                $thePidValue = -1;
2891
            }
2892
        }
2893
        return $thePidValue;
2894
    }
2895
2896
    /**
2897
     * Return the real pid of a record and caches the result.
2898
     * The non-cached method needs database queries to do the job, so this method
2899
     * can be used if code sometimes calls the same record multiple times to save
2900
     * some queries. This should not be done if the calling code may change the
2901
     * same record meanwhile.
2902
     *
2903
     * @param string $table Tablename
2904
     * @param string $uid UID value
2905
     * @param string $pid PID value
2906
     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
2907
     */
2908
    public static function getTSCpidCached($table, $uid, $pid)
2909
    {
2910
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
2911
        $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: [];
2912
        $key = $table . ':' . $uid . ':' . $pid;
2913
        if (!isset($firstLevelCache[$key])) {
2914
            $firstLevelCache[$key] = static::getTSCpid($table, (int)$uid, (int)$pid);
2915
            $runtimeCache->set('backendUtilityTscPidCached', $firstLevelCache);
2916
        }
2917
        return $firstLevelCache[$key];
2918
    }
2919
2920
    /**
2921
     * Returns the REAL pid of the record, if possible. If both $uid and $pid is strings, then pid=-1 is returned as an error indication.
2922
     *
2923
     * @param string $table Table name
2924
     * @param int $uid Record uid
2925
     * @param int|string $pid Record pid
2926
     * @return array Array of two integers; first is the REAL PID of a record and if its a new record negative values are resolved to the true PID,
2927
     * second value is the PID value for TSconfig (uid if table is pages, otherwise the pid)
2928
     * @internal
2929
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::setHistory()
2930
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::process_datamap()
2931
     */
2932
    public static function getTSCpid($table, $uid, $pid)
2933
    {
2934
        // If pid is negative (referring to another record) the pid of the other record is fetched and returned.
2935
        $cPid = self::getTSconfig_pidValue($table, $uid, $pid);
2936
        // $TScID is the id of $table = pages, else it's the pid of the record.
2937
        $TScID = $table === 'pages' && MathUtility::canBeInterpretedAsInteger($uid) ? $uid : $cPid;
2938
        return [$TScID, $cPid];
2939
    }
2940
2941
    /**
2942
     * Gets an instance of the runtime cache.
2943
     *
2944
     * @return FrontendInterface
2945
     */
2946
    protected static function getRuntimeCache()
2947
    {
2948
        return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
2949
    }
2950
2951
    /**
2952
     * Returns TRUE if $modName is set and is found as a main- or submodule in $TBE_MODULES array
2953
     *
2954
     * @param string $modName Module name
2955
     * @return bool
2956
     */
2957
    public static function isModuleSetInTBE_MODULES($modName)
2958
    {
2959
        $loaded = [];
2960
        foreach ($GLOBALS['TBE_MODULES'] as $mkey => $list) {
2961
            $loaded[$mkey] = 1;
2962
            if (!is_array($list) && trim($list)) {
2963
                $subList = GeneralUtility::trimExplode(',', $list, true);
2964
                foreach ($subList as $skey) {
2965
                    $loaded[$mkey . '_' . $skey] = 1;
2966
                }
2967
            }
2968
        }
2969
        return $modName && isset($loaded[$modName]);
2970
    }
2971
2972
    /**
2973
     * Counting references to a record/file
2974
     *
2975
     * @param string $table Table name (or "_FILE" if its a file)
2976
     * @param string $ref Reference: If table, then int-uid, if _FILE, then file reference (relative to Environment::getPublicPath())
2977
     * @param string $msg Message with %s, eg. "There were %s records pointing to this file!
2978
     * @param string|int|null $count Reference count
2979
     * @return string|int Output string (or int count value if no msg string specified)
2980
     */
2981
    public static function referenceCount($table, $ref, $msg = '', $count = null)
2982
    {
2983
        if ($count === null) {
2984
2985
            // Build base query
2986
            $queryBuilder = static::getQueryBuilderForTable('sys_refindex');
2987
            $queryBuilder
2988
                ->count('*')
2989
                ->from('sys_refindex')
2990
                ->where(
2991
                    $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR))
2992
                );
2993
2994
            // Look up the path:
2995
            if ($table === '_FILE') {
2996
                if (!str_starts_with($ref, Environment::getPublicPath())) {
2997
                    return '';
2998
                }
2999
3000
                $ref = PathUtility::stripPathSitePrefix($ref);
3001
                $queryBuilder->andWhere(
3002
                    $queryBuilder->expr()->eq('ref_string', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_STR))
3003
                );
3004
            } else {
3005
                $queryBuilder->andWhere(
3006
                    $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT))
3007
                );
3008
                if ($table === 'sys_file') {
3009
                    $queryBuilder->andWhere($queryBuilder->expr()->neq('tablename', $queryBuilder->quote('sys_file_metadata')));
3010
                }
3011
            }
3012
3013
            $count = $queryBuilder->execute()->fetchOne();
3014
        }
3015
3016
        if ($count) {
3017
            return $msg ? sprintf($msg, $count) : $count;
3018
        }
3019
        return $msg ? '' : 0;
3020
    }
3021
3022
    /**
3023
     * Counting translations of records
3024
     *
3025
     * @param string $table Table name
3026
     * @param string $ref Reference: the record's uid
3027
     * @param string $msg Message with %s, eg. "This record has %s translation(s) which will be deleted, too!
3028
     * @return string Output string (or int count value if no msg string specified)
3029
     */
3030
    public static function translationCount($table, $ref, $msg = '')
3031
    {
3032
        $count = 0;
3033
        if (($GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? null)
3034
            && ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null)
3035
        ) {
3036
            $queryBuilder = static::getQueryBuilderForTable($table);
3037
            $queryBuilder->getRestrictions()
3038
                ->removeAll()
3039
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3040
3041
            $count = (int)$queryBuilder
3042
                ->count('*')
3043
                ->from($table)
3044
                ->where(
3045
                    $queryBuilder->expr()->eq(
3046
                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3047
                        $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)
3048
                    ),
3049
                    $queryBuilder->expr()->neq(
3050
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
3051
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3052
                    )
3053
                )
3054
                ->execute()
3055
                ->fetchOne();
3056
        }
3057
3058
        if ($count > 0) {
3059
            return $msg ? sprintf($msg, $count) : (string)$count;
3060
        }
3061
        return $msg ? '' : '0';
3062
    }
3063
3064
    /*******************************************
3065
     *
3066
     * Workspaces / Versioning
3067
     *
3068
     *******************************************/
3069
    /**
3070
     * Select all versions of a record, ordered by latest created version (uid DESC)
3071
     *
3072
     * @param string $table Table name to select from
3073
     * @param int $uid Record uid for which to find versions.
3074
     * @param string $fields Field list to select
3075
     * @param int|null $workspace Search in workspace ID and Live WS, if 0 search only in LiveWS, if NULL search in all WS.
3076
     * @param bool $includeDeletedRecords If set, deleted-flagged versions are included! (Only for clean-up script!)
3077
     * @param array $row The current record
3078
     * @return array|null Array of versions of table/uid
3079
     * @internal should only be used from within TYPO3 Core
3080
     */
3081
    public static function selectVersionsOfRecord(
3082
        $table,
3083
        $uid,
3084
        $fields = '*',
3085
        $workspace = 0,
3086
        $includeDeletedRecords = false,
3087
        $row = null
3088
    ) {
3089
        $outputRows = [];
3090
        if (static::isTableWorkspaceEnabled($table)) {
3091
            if (is_array($row) && !$includeDeletedRecords) {
3092
                $row['_CURRENT_VERSION'] = true;
3093
                $outputRows[] = $row;
3094
            } else {
3095
                // Select UID version:
3096
                $row = self::getRecord($table, $uid, $fields, '', !$includeDeletedRecords);
3097
                // Add rows to output array:
3098
                if ($row) {
3099
                    $row['_CURRENT_VERSION'] = true;
3100
                    $outputRows[] = $row;
3101
                }
3102
            }
3103
3104
            $queryBuilder = static::getQueryBuilderForTable($table);
3105
            $queryBuilder->getRestrictions()->removeAll();
3106
3107
            // build fields to select
3108
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3109
3110
            $queryBuilder
3111
                ->from($table)
3112
                ->where(
3113
                    $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
3114
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
3115
                )
3116
                ->orderBy('uid', 'DESC');
3117
3118
            if (!$includeDeletedRecords) {
3119
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3120
            }
3121
3122
            if ($workspace === 0) {
3123
                // Only in Live WS
3124
                $queryBuilder->andWhere(
3125
                    $queryBuilder->expr()->eq(
3126
                        't3ver_wsid',
3127
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3128
                    )
3129
                );
3130
            } elseif ($workspace !== null) {
3131
                // In Live WS and Workspace with given ID
3132
                $queryBuilder->andWhere(
3133
                    $queryBuilder->expr()->in(
3134
                        't3ver_wsid',
3135
                        $queryBuilder->createNamedParameter([0, (int)$workspace], Connection::PARAM_INT_ARRAY)
3136
                    )
3137
                );
3138
            }
3139
3140
            $rows = $queryBuilder->execute()->fetchAllAssociative();
3141
3142
            // Add rows to output array:
3143
            if (is_array($rows)) {
0 ignored issues
show
introduced by
The condition is_array($rows) is always true.
Loading history...
3144
                $outputRows = array_merge($outputRows, $rows);
3145
            }
3146
            return $outputRows;
3147
        }
3148
        return null;
3149
    }
3150
3151
    /**
3152
     * Workspace Preview Overlay.
3153
     *
3154
     * Generally ALWAYS used when records are selected based on uid or pid.
3155
     * Principle; Record online! => Find offline?
3156
     * The function MAY set $row to FALSE. This happens if a moved record is given and
3157
     * $unsetMovePointers is set to true. In other words, you should check if the input record
3158
     * is still an array afterwards when using this function.
3159
     *
3160
     * If the versioned record is a moved record the "pid" value will then contain the newly moved location
3161
     * and "ORIG_pid" will contain the live pid.
3162
     *
3163
     * @param string $table Table name
3164
     * @param array $row Record by reference. At least "uid", "pid", "t3ver_oid" and "t3ver_state" must be set. Keys not prefixed with '_' are used as field names in SQL.
3165
     * @param int $wsid Workspace ID, if not specified will use static::getBackendUserAuthentication()->workspace
3166
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
3167
     */
3168
    public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false)
3169
    {
3170
        if (!ExtensionManagementUtility::isLoaded('workspaces') || !is_array($row) || !static::isTableWorkspaceEnabled($table)) {
3171
            return;
3172
        }
3173
3174
        // Initialize workspace ID
3175
        $wsid = (int)$wsid;
3176
        if ($wsid === -99 && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3177
            $wsid = (int)static::getBackendUserAuthentication()->workspace;
3178
        }
3179
        if ($wsid === 0) {
3180
            // Return early if in live workspace
3181
            return;
3182
        }
3183
3184
        // Check if input record is a moved record
3185
        $incomingRecordIsAMoveVersion = false;
3186
        if (isset($row['t3ver_oid'], $row['t3ver_state'])
3187
            && $row['t3ver_oid'] > 0
3188
            && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER
3189
        ) {
3190
            // @todo: This handling needs a review, together with the 4th param $unsetMovePointers
3191
            $incomingRecordIsAMoveVersion = true;
3192
        }
3193
3194
        $wsAlt = self::getWorkspaceVersionOfRecord(
3195
            $wsid,
3196
            $table,
3197
            $row['uid'],
3198
            implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3199
        );
3200
3201
        // If version was found, swap the default record with that one.
3202
        if (is_array($wsAlt)) {
3203
            // If t3ver_state is not found, then find it... (but we like best if it is here...)
3204
            if (!isset($wsAlt['t3ver_state'])) {
3205
                $stateRec = self::getRecord($table, $wsAlt['uid'], 't3ver_state');
3206
                $versionState = VersionState::cast($stateRec['t3ver_state']);
3207
            } else {
3208
                $versionState = VersionState::cast($wsAlt['t3ver_state']);
3209
            }
3210
            // Check if this is in move-state
3211
            if ($versionState->equals(VersionState::MOVE_POINTER)) {
3212
                // @todo Same problem as frontend in versionOL(). See TODO point there and todo above.
3213
                if (!$incomingRecordIsAMoveVersion && $unsetMovePointers) {
3214
                    $row = false;
3215
                    return;
3216
                }
3217
                // When a moved record is found the "PID" value contains the newly moved location
3218
                // Whereas the _ORIG_pid field contains the PID of the live version
3219
                $wsAlt['_ORIG_pid'] = $row['pid'];
3220
            }
3221
            // Swap UID
3222
            if (!$versionState->equals(VersionState::NEW_PLACEHOLDER)) {
3223
                $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
3224
                $wsAlt['uid'] = $row['uid'];
3225
            }
3226
            // Backend css class:
3227
            $wsAlt['_CSSCLASS'] = 'ver-element';
3228
            // Changing input record to the workspace version alternative:
3229
            $row = $wsAlt;
3230
        }
3231
    }
3232
3233
    /**
3234
     * Select the workspace version of a record, if exists
3235
     *
3236
     * @param int $workspace Workspace ID
3237
     * @param string $table Table name to select from
3238
     * @param int $uid Record uid for which to find workspace version.
3239
     * @param string $fields Field list to select
3240
     * @return array|bool If found, return record, otherwise false
3241
     */
3242
    public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*')
3243
    {
3244
        if (ExtensionManagementUtility::isLoaded('workspaces')) {
3245
            if ($workspace !== 0 && self::isTableWorkspaceEnabled($table)) {
3246
3247
                // Select workspace version of record:
3248
                $queryBuilder = static::getQueryBuilderForTable($table);
3249
                $queryBuilder->getRestrictions()
3250
                    ->removeAll()
3251
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3252
3253
                // build fields to select
3254
                $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3255
3256
                $row = $queryBuilder
3257
                    ->from($table)
3258
                    ->where(
3259
                        $queryBuilder->expr()->eq(
3260
                            't3ver_wsid',
3261
                            $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
3262
                        ),
3263
                        $queryBuilder->expr()->orX(
3264
                            // t3ver_state=1 does not contain a t3ver_oid, and returns itself
3265
                            $queryBuilder->expr()->andX(
3266
                                $queryBuilder->expr()->eq(
3267
                                    'uid',
3268
                                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3269
                                ),
3270
                                $queryBuilder->expr()->eq(
3271
                                    't3ver_state',
3272
                                    $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER, \PDO::PARAM_INT)
3273
                                )
3274
                            ),
3275
                            $queryBuilder->expr()->eq(
3276
                                't3ver_oid',
3277
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3278
                            )
3279
                        )
3280
                    )
3281
                    ->execute()
3282
                    ->fetchAssociative();
3283
3284
                return $row;
3285
            }
3286
        }
3287
        return false;
3288
    }
3289
3290
    /**
3291
     * Returns live version of record
3292
     *
3293
     * @param string $table Table name
3294
     * @param int|string $uid Record UID of draft, offline version
3295
     * @param string $fields Field list, default is *
3296
     * @return array|null If found, the record, otherwise NULL
3297
     */
3298
    public static function getLiveVersionOfRecord($table, $uid, $fields = '*')
3299
    {
3300
        $liveVersionId = self::getLiveVersionIdOfRecord($table, $uid);
3301
        if ($liveVersionId !== null) {
3302
            return self::getRecord($table, $liveVersionId, $fields);
3303
        }
3304
        return null;
3305
    }
3306
3307
    /**
3308
     * Gets the id of the live version of a record.
3309
     *
3310
     * @param string $table Name of the table
3311
     * @param int|string $uid Uid of the offline/draft record
3312
     * @return int|null The id of the live version of the record (or NULL if nothing was found)
3313
     * @internal should only be used from within TYPO3 Core
3314
     */
3315
    public static function getLiveVersionIdOfRecord($table, $uid)
3316
    {
3317
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3318
            return null;
3319
        }
3320
        $liveVersionId = null;
3321
        if (self::isTableWorkspaceEnabled($table)) {
3322
            $currentRecord = self::getRecord($table, $uid, 'pid,t3ver_oid,t3ver_state');
3323
            if (is_array($currentRecord)) {
3324
                if ((int)$currentRecord['t3ver_oid'] > 0) {
3325
                    $liveVersionId = $currentRecord['t3ver_oid'];
3326
                } elseif ((int)($currentRecord['t3ver_state']) === VersionState::NEW_PLACEHOLDER) {
3327
                    // New versions do not have a live counterpart
3328
                    $liveVersionId = (int)$uid;
3329
                }
3330
            }
3331
        }
3332
        return $liveVersionId;
3333
    }
3334
3335
    /**
3336
     * Performs mapping of new uids to new versions UID in case of import inside a workspace.
3337
     *
3338
     * @param string $table Table name
3339
     * @param int $uid Record uid (of live record placeholder)
3340
     * @return int Uid of offline version if any, otherwise live uid.
3341
     * @internal should only be used from within TYPO3 Core
3342
     */
3343
    public static function wsMapId($table, $uid)
3344
    {
3345
        $wsRec = null;
3346
        if (static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3347
            $wsRec = self::getWorkspaceVersionOfRecord(
3348
                static::getBackendUserAuthentication()->workspace,
3349
                $table,
3350
                $uid,
3351
                'uid'
3352
            );
3353
        }
3354
        return is_array($wsRec) ? $wsRec['uid'] : $uid;
3355
    }
3356
3357
    /*******************************************
3358
     *
3359
     * Miscellaneous
3360
     *
3361
     *******************************************/
3362
3363
    /**
3364
     * Creates ADMCMD parameters for the "viewpage" extension / frontend
3365
     *
3366
     * @param array $pageInfo Page record
3367
     * @param \TYPO3\CMS\Core\Context\Context $context
3368
     * @return string Query-parameters
3369
     * @internal
3370
     */
3371
    public static function ADMCMD_previewCmds($pageInfo, Context $context)
3372
    {
3373
        if ($pageInfo === []) {
3374
            return '';
3375
        }
3376
        // Initialize access restriction values from current page
3377
        $access = [
3378
            'fe_group' => (string)($pageInfo['fe_group'] ?? ''),
3379
            'starttime' => (int)($pageInfo['starttime'] ?? 0),
3380
            'endtime' => (int)($pageInfo['endtime'] ?? 0),
3381
        ];
3382
        // Only check rootline if the current page has not set extendToSubpages itself
3383
        if (!(bool)($pageInfo['extendToSubpages'] ?? false)) {
3384
            $rootline = self::BEgetRootLine((int)($pageInfo['uid'] ?? 0));
3385
            // remove the current page from the rootline
3386
            array_shift($rootline);
3387
            foreach ($rootline as $page) {
3388
                // Skip root node, invalid pages and pages which do not define extendToSubpages
3389
                if ((int)($page['uid'] ?? 0) <= 0 || !(bool)($page['extendToSubpages'] ?? false)) {
3390
                    continue;
3391
                }
3392
                $access['fe_group'] = (string)($page['fe_group'] ?? '');
3393
                $access['starttime'] = (int)($page['starttime'] ?? 0);
3394
                $access['endtime'] = (int)($page['endtime'] ?? 0);
3395
                // Stop as soon as a page in the rootline has extendToSubpages set
3396
                break;
3397
            }
3398
        }
3399
        $simUser = '';
3400
        $simTime = '';
3401
        if ((int)$access['fe_group'] === -2) {
3402
            // -2 means "show at any login". We simulate first available fe_group.
3403
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3404
                ->getQueryBuilderForTable('fe_groups');
3405
            $queryBuilder->getRestrictions()
3406
                ->removeAll()
3407
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3408
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
3409
3410
            $activeFeGroupId = $queryBuilder->select('uid')
3411
                ->from('fe_groups')
3412
                ->execute()
3413
                ->fetchOne();
3414
3415
            if ($activeFeGroupId) {
3416
                $simUser = '&ADMCMD_simUser=' . $activeFeGroupId;
3417
            }
3418
        } elseif (!empty($access['fe_group'])) {
3419
            $simUser = '&ADMCMD_simUser=' . $access['fe_group'];
3420
        }
3421
        if ($access['starttime'] > $GLOBALS['EXEC_TIME']) {
3422
            // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
3423
            // an URL for it
3424
            $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $access['starttime']));
3425
            $context->setAspect('date', $dateAspect);
3426
            $simTime = '&ADMCMD_simTime=' . $access['starttime'];
3427
        }
3428
        if ($access['endtime'] < $GLOBALS['EXEC_TIME'] && $access['endtime'] !== 0) {
3429
            // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
3430
            // in turn PageRouter will generate an URL for it
3431
            $dateAspect = GeneralUtility::makeInstance(
3432
                DateTimeAspect::class,
3433
                new \DateTimeImmutable('@' . ($access['endtime'] - 1))
3434
            );
3435
            $context->setAspect('date', $dateAspect);
3436
            $simTime = '&ADMCMD_simTime=' . ($access['endtime'] - 1);
3437
        }
3438
        return $simUser . $simTime;
3439
    }
3440
3441
    /**
3442
     * Determines whether a table is enabled for workspaces.
3443
     *
3444
     * @param string $table Name of the table to be checked
3445
     * @return bool
3446
     */
3447
    public static function isTableWorkspaceEnabled($table)
3448
    {
3449
        return !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']);
3450
    }
3451
3452
    /**
3453
     * Gets the TCA configuration of a field.
3454
     *
3455
     * @param string $table Name of the table
3456
     * @param string $field Name of the field
3457
     * @return array
3458
     */
3459
    public static function getTcaFieldConfiguration($table, $field)
3460
    {
3461
        $configuration = [];
3462
        if (isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
3463
            $configuration = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3464
        }
3465
        return $configuration;
3466
    }
3467
3468
    /**
3469
     * Whether to ignore restrictions on a web-mount of a table.
3470
     * The regular behaviour is that records to be accessed need to be
3471
     * in a valid user's web-mount.
3472
     *
3473
     * @param string $table Name of the table
3474
     * @return bool
3475
     */
3476
    public static function isWebMountRestrictionIgnored($table)
3477
    {
3478
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreWebMountRestriction']);
3479
    }
3480
3481
    /**
3482
     * Whether to ignore restrictions on root-level records.
3483
     * The regular behaviour is that records on the root-level (page-id 0)
3484
     * only can be accessed by admin users.
3485
     *
3486
     * @param string $table Name of the table
3487
     * @return bool
3488
     */
3489
    public static function isRootLevelRestrictionIgnored($table)
3490
    {
3491
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']);
3492
    }
3493
3494
    /**
3495
     * Get all fields of a table, which are allowed for the current user
3496
     *
3497
     * @param string $table Table name
3498
     * @param bool $checkUserAccess If set, users access to the field (non-exclude-fields) is checked.
3499
     * @return string[] Array, where values are fieldnames
3500
     * @internal should only be used from within TYPO3 Core
3501
     */
3502
    public static function getAllowedFieldsForTable(string $table, bool $checkUserAccess = true): array
3503
    {
3504
        if (!is_array($GLOBALS['TCA'][$table]['columns'] ?? null)) {
3505
            self::getLogger()->error('TCA is broken for the table "' . $table . '": no required "columns" entry in TCA.');
3506
            return [];
3507
        }
3508
3509
        $fieldList = [];
3510
        $backendUser = self::getBackendUserAuthentication();
3511
3512
        // Traverse configured columns and add them to field array, if available for user.
3513
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
3514
            if (($fieldValue['config']['type'] ?? '') === 'none') {
3515
                // Never render or fetch type=none fields from db
3516
                continue;
3517
            }
3518
            if (!$checkUserAccess
3519
                || (
3520
                    (
3521
                        !($fieldValue['exclude'] ?? null)
3522
                        || $backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
3523
                    )
3524
                    && ($fieldValue['config']['type'] ?? '') !== 'passthrough'
3525
                )
3526
            ) {
3527
                $fieldList[] = $fieldName;
3528
            }
3529
        }
3530
3531
        $fieldList[] = 'uid';
3532
        $fieldList[] = 'pid';
3533
3534
        // Add more special fields (e.g. date fields) if user should not be checked or is admin
3535
        if (!$checkUserAccess || $backendUser->isAdmin()) {
3536
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? false) {
3537
                $fieldList[] = $GLOBALS['TCA'][$table]['ctrl']['tstamp'];
3538
            }
3539
            if ($GLOBALS['TCA'][$table]['ctrl']['crdate'] ?? false) {
3540
                $fieldList[] = $GLOBALS['TCA'][$table]['ctrl']['crdate'];
3541
            }
3542
            if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id'] ?? false) {
3543
                $fieldList[] = $GLOBALS['TCA'][$table]['ctrl']['cruser_id'];
3544
            }
3545
            if ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? false) {
3546
                $fieldList[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
3547
            }
3548
            if (self::isTableWorkspaceEnabled($table)) {
3549
                $fieldList[] = 't3ver_state';
3550
                $fieldList[] = 't3ver_wsid';
3551
                $fieldList[] = 't3ver_oid';
3552
            }
3553
        }
3554
3555
        // Return unique field list
3556
        return array_values(array_unique($fieldList));
3557
    }
3558
3559
    /**
3560
     * @param string $table
3561
     * @return Connection
3562
     */
3563
    protected static function getConnectionForTable($table)
3564
    {
3565
        return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
3566
    }
3567
3568
    /**
3569
     * @param string $table
3570
     * @return QueryBuilder
3571
     */
3572
    protected static function getQueryBuilderForTable($table)
3573
    {
3574
        return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3575
    }
3576
3577
    /**
3578
     * @return LoggerInterface
3579
     */
3580
    protected static function getLogger()
3581
    {
3582
        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
3583
    }
3584
3585
    /**
3586
     * @return LanguageService
3587
     */
3588
    protected static function getLanguageService()
3589
    {
3590
        return $GLOBALS['LANG'];
3591
    }
3592
3593
    /**
3594
     * @return BackendUserAuthentication|null
3595
     */
3596
    protected static function getBackendUserAuthentication()
3597
    {
3598
        return $GLOBALS['BE_USER'] ?? null;
3599
    }
3600
}
3601