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
16namespace TYPO3\CMS\Backend\Utility;
17
18use Psr\EventDispatcher\EventDispatcherInterface;
19use Psr\Log\LoggerInterface;
20use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
21use TYPO3\CMS\Backend\Routing\UriBuilder;
22use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23use TYPO3\CMS\Core\Cache\CacheManager;
24use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
25use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent;
26use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
27use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
28use TYPO3\CMS\Core\Context\Context;
29use TYPO3\CMS\Core\Context\DateTimeAspect;
30use TYPO3\CMS\Core\Core\Environment;
31use TYPO3\CMS\Core\Database\Connection;
32use TYPO3\CMS\Core\Database\ConnectionPool;
33use TYPO3\CMS\Core\Database\Query\QueryBuilder;
34use TYPO3\CMS\Core\Database\Query\QueryHelper;
35use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
36use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
37use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
38use TYPO3\CMS\Core\Database\RelationHandler;
39use TYPO3\CMS\Core\Domain\Repository\PageRepository;
40use TYPO3\CMS\Core\Exception\SiteNotFoundException;
41use TYPO3\CMS\Core\Http\Uri;
42use TYPO3\CMS\Core\Imaging\Icon;
43use TYPO3\CMS\Core\Imaging\IconFactory;
44use TYPO3\CMS\Core\Imaging\ImageDimension;
45use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
46use TYPO3\CMS\Core\Information\Typo3Information;
47use TYPO3\CMS\Core\Localization\LanguageService;
48use TYPO3\CMS\Core\Log\LogManager;
49use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
50use TYPO3\CMS\Core\Resource\ProcessedFile;
51use TYPO3\CMS\Core\Resource\ResourceFactory;
52use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
53use TYPO3\CMS\Core\Routing\RouterInterface;
54use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
55use TYPO3\CMS\Core\Site\SiteFinder;
56use TYPO3\CMS\Core\Type\Bitmask\Permission;
57use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
58use TYPO3\CMS\Core\Utility\ArrayUtility;
59use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
60use TYPO3\CMS\Core\Utility\GeneralUtility;
61use TYPO3\CMS\Core\Utility\HttpUtility;
62use TYPO3\CMS\Core\Utility\MathUtility;
63use TYPO3\CMS\Core\Utility\PathUtility;
64use TYPO3\CMS\Core\Versioning\VersionState;
65
66/**
67 * Standard functions available for the TYPO3 backend.
68 * You are encouraged to use this class in your own applications (Backend Modules)
69 * Don't instantiate - call functions with "\TYPO3\CMS\Backend\Utility\BackendUtility::" prefixed the function name.
70 *
71 * Call ALL methods without making an object!
72 * Eg. to get a page-record 51 do this: '\TYPO3\CMS\Backend\Utility\BackendUtility::getRecord('pages',51)'
73 */
74class BackendUtility
75{
76    /*******************************************
77     *
78     * SQL-related, selecting records, searching
79     *
80     *******************************************/
81    /**
82     * Gets record with uid = $uid from $table
83     * You can set $field to a list of fields (default is '*')
84     * Additional WHERE clauses can be added by $where (fx. ' AND some_field = 1')
85     * Will automatically check if records has been deleted and if so, not return anything.
86     * $table must be found in $GLOBALS['TCA']
87     *
88     * @param string $table Table name present in $GLOBALS['TCA']
89     * @param int $uid UID of record
90     * @param string $fields List of fields to select
91     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
92     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
93     * @return array|null Returns the row if found, otherwise NULL
94     */
95    public static function getRecord($table, $uid, $fields = '*', $where = '', $useDeleteClause = true)
96    {
97        // Ensure we have a valid uid (not 0 and not NEWxxxx) and a valid TCA
98        if ((int)$uid && !empty($GLOBALS['TCA'][$table])) {
99            $queryBuilder = static::getQueryBuilderForTable($table);
100
101            // do not use enabled fields here
102            $queryBuilder->getRestrictions()->removeAll();
103
104            // should the delete clause be used
105            if ($useDeleteClause) {
106                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
107            }
108
109            // set table and where clause
110            $queryBuilder
111                ->select(...GeneralUtility::trimExplode(',', $fields, true))
112                ->from($table)
113                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)));
114
115            // add custom where clause
116            if ($where) {
117                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
118            }
119
120            $row = $queryBuilder->execute()->fetch();
121            if ($row) {
122                return $row;
123            }
124        }
125        return null;
126    }
127
128    /**
129     * Like getRecord(), but overlays workspace version if any.
130     *
131     * @param string $table Table name present in $GLOBALS['TCA']
132     * @param int $uid UID of record
133     * @param string $fields List of fields to select
134     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
135     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
136     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
137     * @return array Returns the row if found, otherwise nothing
138     */
139    public static function getRecordWSOL(
140        $table,
141        $uid,
142        $fields = '*',
143        $where = '',
144        $useDeleteClause = true,
145        $unsetMovePointers = false
146    ) {
147        if ($fields !== '*') {
148            $internalFields = GeneralUtility::uniqueList($fields . ',uid,pid');
149            $row = self::getRecord($table, $uid, $internalFields, $where, $useDeleteClause);
150            self::workspaceOL($table, $row, -99, $unsetMovePointers);
151            if (is_array($row)) {
152                foreach ($row as $key => $_) {
153                    if (!GeneralUtility::inList($fields, $key) && $key[0] !== '_') {
154                        unset($row[$key]);
155                    }
156                }
157            }
158        } else {
159            $row = self::getRecord($table, $uid, $fields, $where, $useDeleteClause);
160            self::workspaceOL($table, $row, -99, $unsetMovePointers);
161        }
162        return $row;
163    }
164
165    /**
166     * Purges computed properties starting with underscore character ('_').
167     *
168     * @param array<string,mixed> $record
169     * @return array<string,mixed>
170     * @internal should only be used from within TYPO3 Core
171     */
172    public static function purgeComputedPropertiesFromRecord(array $record): array
173    {
174        return array_filter(
175            $record,
176            function (string $propertyName): bool {
177                return $propertyName[0] !== '_';
178            },
179            ARRAY_FILTER_USE_KEY
180        );
181    }
182
183    /**
184     * Purges computed property names starting with underscore character ('_').
185     *
186     * @param array $propertyNames
187     * @return array
188     * @internal should only be used from within TYPO3 Core
189     */
190    public static function purgeComputedPropertyNames(array $propertyNames): array
191    {
192        return array_filter(
193            $propertyNames,
194            function (string $propertyName): bool {
195                return $propertyName[0] !== '_';
196            }
197        );
198    }
199
200    /**
201     * Makes a backwards explode on the $str and returns an array with ($table, $uid).
202     * Example: tt_content_45 => ['tt_content', 45]
203     *
204     * @param string $str [tablename]_[uid] string to explode
205     * @return array
206     * @internal should only be used from within TYPO3 Core
207     */
208    public static function splitTable_Uid($str)
209    {
210        [$uid, $table] = explode('_', strrev($str), 2);
211        return [strrev($table), strrev($uid)];
212    }
213
214    /**
215     * Backend implementation of enableFields()
216     * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime.
217     * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition.
218     * $GLOBALS["SIM_ACCESS_TIME"] is used for date.
219     *
220     * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA'].
221     * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection)
222     * @return string WHERE clause part
223     * @internal should only be used from within TYPO3 Core, but DefaultRestrictionHandler is recommended as alternative
224     */
225    public static function BEenableFields($table, $inv = false)
226    {
227        $ctrl = $GLOBALS['TCA'][$table]['ctrl'];
228        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
229            ->getConnectionForTable($table)
230            ->getExpressionBuilder();
231        $query = $expressionBuilder->andX();
232        $invQuery = $expressionBuilder->orX();
233
234        if (is_array($ctrl)) {
235            if (is_array($ctrl['enablecolumns'])) {
236                if ($ctrl['enablecolumns']['disabled'] ?? false) {
237                    $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
238                    $query->add($expressionBuilder->eq($field, 0));
239                    $invQuery->add($expressionBuilder->neq($field, 0));
240                }
241                if ($ctrl['enablecolumns']['starttime'] ?? false) {
242                    $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
243                    $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']));
244                    $invQuery->add(
245                        $expressionBuilder->andX(
246                            $expressionBuilder->neq($field, 0),
247                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
248                        )
249                    );
250                }
251                if ($ctrl['enablecolumns']['endtime'] ?? false) {
252                    $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
253                    $query->add(
254                        $expressionBuilder->orX(
255                            $expressionBuilder->eq($field, 0),
256                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
257                        )
258                    );
259                    $invQuery->add(
260                        $expressionBuilder->andX(
261                            $expressionBuilder->neq($field, 0),
262                            $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
263                        )
264                    );
265                }
266            }
267        }
268
269        if ($query->count() === 0) {
270            return '';
271        }
272
273        return ' AND ' . ($inv ? $invQuery : $query);
274    }
275
276    /**
277     * Fetches the localization for a given record.
278     *
279     * @param string $table Table name present in $GLOBALS['TCA']
280     * @param int $uid The uid of the record
281     * @param int $language The uid of the language record in sys_language
282     * @param string $andWhereClause Optional additional WHERE clause (default: '')
283     * @return mixed Multidimensional array with selected records, empty array if none exists and FALSE if table is not localizable
284     */
285    public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
286    {
287        $recordLocalization = false;
288
289        if (self::isTableLocalizable($table)) {
290            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
291
292            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
293                ->getQueryBuilderForTable($table);
294            $queryBuilder->getRestrictions()
295                ->removeAll()
296                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
297                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace ?? 0));
298
299            $queryBuilder->select('*')
300                ->from($table)
301                ->where(
302                    $queryBuilder->expr()->eq(
303                        $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'],
304                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
305                    ),
306                    $queryBuilder->expr()->eq(
307                        $tcaCtrl['languageField'],
308                        $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
309                    )
310                )
311                ->setMaxResults(1);
312
313            if ($andWhereClause) {
314                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
315            }
316
317            $recordLocalization = $queryBuilder->execute()->fetchAll();
318        }
319
320        return $recordLocalization;
321    }
322
323    /*******************************************
324     *
325     * Page tree, TCA related
326     *
327     *******************************************/
328    /**
329     * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id
330     * ($uid) and back to the root.
331     * By default deleted pages are filtered.
332     * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known
333     * from the frontend where the rootline stops when a root-template is found.
334     *
335     * @param int $uid Page id for which to create the root line.
336     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that
337     *          stops the process if we meet a page, the user has no reading access to.
338     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is
339     *          usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
340     * @param string[] $additionalFields Additional Fields to select for rootline records
341     * @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
342     */
343    public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = [])
344    {
345        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
346        $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: [];
347        $output = [];
348        $pid = $uid;
349        $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . md5(implode(',', $additionalFields)) : '');
350        if (is_array($beGetRootLineCache[$ident] ?? false)) {
351            $output = $beGetRootLineCache[$ident];
352        } else {
353            $loopCheck = 100;
354            $theRowArray = [];
355            while ($uid != 0 && $loopCheck) {
356                $loopCheck--;
357                $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields);
358                if (is_array($row)) {
359                    $uid = $row['pid'];
360                    $theRowArray[] = $row;
361                } else {
362                    break;
363                }
364            }
365            $fields = [
366                'uid',
367                'pid',
368                'title',
369                'doktype',
370                'slug',
371                'tsconfig_includes',
372                'TSconfig',
373                'is_siteroot',
374                't3ver_oid',
375                't3ver_wsid',
376                't3ver_state',
377                't3ver_stage',
378                'backend_layout_next_level',
379                'hidden',
380                'starttime',
381                'endtime',
382                'fe_group',
383                'nav_hide',
384                'content_from_pid',
385                'module',
386                'extendToSubpages'
387            ];
388            $fields = array_merge($fields, $additionalFields);
389            $rootPage = array_fill_keys($fields, null);
390            if ($uid == 0) {
391                $rootPage['uid'] = 0;
392                $theRowArray[] = $rootPage;
393            }
394            $c = count($theRowArray);
395            foreach ($theRowArray as $val) {
396                $c--;
397                $output[$c] = array_intersect_key($val, $rootPage);
398                if (isset($val['_ORIG_pid'])) {
399                    $output[$c]['_ORIG_pid'] = $val['_ORIG_pid'];
400                }
401            }
402            $beGetRootLineCache[$ident] = $output;
403            $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache);
404        }
405        return $output;
406    }
407
408    /**
409     * Gets the cached page record for the rootline
410     *
411     * @param int $uid Page id for which to create the root line.
412     * @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.
413     * @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!
414     * @param string[] $additionalFields AdditionalFields to fetch from the root line
415     * @return array Cached page record for the rootline
416     * @see BEgetRootLine
417     */
418    protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = [])
419    {
420        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
421        $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: [];
422        $statementCacheIdent = md5($clause . ($additionalFields ? '-' . implode(',', $additionalFields) : ''));
423        $ident = $uid . '-' . $workspaceOL . '-' . $statementCacheIdent;
424        if (is_array($pageForRootlineCache[$ident] ?? false)) {
425            $row = $pageForRootlineCache[$ident];
426        } else {
427            $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
428            if (!$statement) {
429                $queryBuilder = static::getQueryBuilderForTable('pages');
430                $queryBuilder->getRestrictions()
431                             ->removeAll()
432                             ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
433
434                $queryBuilder
435                    ->select(
436                        'pid',
437                        'uid',
438                        'title',
439                        'doktype',
440                        'slug',
441                        'tsconfig_includes',
442                        'TSconfig',
443                        'is_siteroot',
444                        't3ver_oid',
445                        't3ver_wsid',
446                        't3ver_state',
447                        't3ver_stage',
448                        'backend_layout_next_level',
449                        'hidden',
450                        'starttime',
451                        'endtime',
452                        'fe_group',
453                        'nav_hide',
454                        'content_from_pid',
455                        'module',
456                        'extendToSubpages',
457                        ...$additionalFields
458                    )
459                    ->from('pages')
460                    ->where(
461                        $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)),
462                        QueryHelper::stripLogicalOperatorPrefix($clause)
463                    );
464                $statement = $queryBuilder->execute();
465                if (class_exists(\Doctrine\DBAL\ForwardCompatibility\Result::class) && $statement instanceof \Doctrine\DBAL\ForwardCompatibility\Result) {
466                    $statement = $statement->getIterator();
467                }
468                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement);
469            } else {
470                $statement->bindValue(1, (int)$uid);
471                $statement->execute();
472            }
473            $row = $statement->fetch();
474            $statement->closeCursor();
475
476            if ($row) {
477                $newLocation = false;
478                if ($workspaceOL) {
479                    self::workspaceOL('pages', $row);
480                    if (is_array($row) && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER) {
481                        $newLocation = self::getMovePlaceholder('pages', $row['uid'], 'pid');
482                    }
483                }
484                if (is_array($row)) {
485                    if ($newLocation !== false) {
486                        $row['pid'] = $newLocation['pid'];
487                    } else {
488                        self::fixVersioningPid('pages', $row);
489                    }
490                    $pageForRootlineCache[$ident] = $row;
491                    $runtimeCache->set('backendUtilityPageForRootLine', $pageForRootlineCache);
492                }
493            }
494        }
495        return $row;
496    }
497
498    /**
499     * Opens the page tree to the specified page id
500     *
501     * @param int $pid Page id.
502     * @param bool $clearExpansion If set, then other open branches are closed.
503     * @internal should only be used from within TYPO3 Core
504     */
505    public static function openPageTree($pid, $clearExpansion)
506    {
507        $beUser = static::getBackendUserAuthentication();
508        // Get current expansion data:
509        if ($clearExpansion) {
510            $expandedPages = [];
511        } else {
512            $expandedPages = $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'];
513        }
514        // Get rootline:
515        $rL = self::BEgetRootLine($pid);
516        // First, find out what mount index to use (if more than one DB mount exists):
517        $mountIndex = 0;
518        $mountKeys = $beUser->returnWebmounts();
519
520        foreach ($rL as $rLDat) {
521            if (isset($mountKeys[$rLDat['uid']])) {
522                $mountIndex = $mountKeys[$rLDat['uid']];
523                break;
524            }
525        }
526        // Traverse rootline and open paths:
527        foreach ($rL as $rLDat) {
528            $expandedPages[$mountIndex . '_' . $rLDat['uid']] = '1';
529        }
530        // Write back:
531        $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'] = $expandedPages;
532        $beUser->writeUC();
533    }
534
535    /**
536     * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage"
537     * Each part of the path will be limited to $titleLimit characters
538     * Deleted pages are filtered out.
539     *
540     * @param int $uid Page uid for which to create record path
541     * @param string $clause Clause is additional where clauses, eg.
542     * @param int $titleLimit Title limit
543     * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so)
544     * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set.
545     */
546    public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0)
547    {
548        if (!$titleLimit) {
549            $titleLimit = 1000;
550        }
551        $output = $fullOutput = '/';
552        $clause = trim($clause);
553        if ($clause !== '' && strpos($clause, 'AND') !== 0) {
554            $clause = 'AND ' . $clause;
555        }
556        $data = self::BEgetRootLine($uid, $clause, true);
557        foreach ($data as $record) {
558            if ($record['uid'] === 0) {
559                continue;
560            }
561            $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output;
562            if ($fullTitleLimit) {
563                $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput;
564            }
565        }
566        if ($fullTitleLimit) {
567            return [$output, $fullOutput];
568        }
569        return $output;
570    }
571
572    /**
573     * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA'].
574     *
575     * @param string $table The table to check
576     * @return bool Whether a table is localizable
577     */
578    public static function isTableLocalizable($table)
579    {
580        $isLocalizable = false;
581        if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) {
582            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
583            $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField'];
584        }
585        return $isLocalizable;
586    }
587
588    /**
589     * 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.
590     * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin.
591     * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause)
592     *
593     * @param int $id Page uid for which to check read-access
594     * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1);
595     * @return array|false Returns page record if OK, otherwise FALSE.
596     */
597    public static function readPageAccess($id, $perms_clause)
598    {
599        if ((string)$id !== '') {
600            $id = (int)$id;
601            if (!$id) {
602                if (static::getBackendUserAuthentication()->isAdmin()) {
603                    return ['_thePath' => '/'];
604                }
605            } else {
606                $pageinfo = self::getRecord('pages', $id, '*', $perms_clause);
607                if ($pageinfo['uid'] && static::getBackendUserAuthentication()->isInWebMount($pageinfo, $perms_clause)) {
608                    self::workspaceOL('pages', $pageinfo);
609                    if (is_array($pageinfo)) {
610                        self::fixVersioningPid('pages', $pageinfo);
611                        [$pageinfo['_thePath'], $pageinfo['_thePathFull']] = self::getRecordPath((int)$pageinfo['uid'], $perms_clause, 15, 1000);
612                        return $pageinfo;
613                    }
614                }
615            }
616        }
617        return false;
618    }
619
620    /**
621     * Returns the "type" value of $rec from $table which can be used to look up the correct "types" rendering section in $GLOBALS['TCA']
622     * If no "type" field is configured in the "ctrl"-section of the $GLOBALS['TCA'] for the table, zero is used.
623     * 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)
624     *
625     * Note: This method is very similar to the type determination of FormDataProvider/DatabaseRecordTypeValue,
626     * however, it has two differences:
627     * 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).
628     * 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"
629     * and "group" field values are stored, which makes different processing of the "foreign pointer field" type field variant necessary.
630     *
631     * @param string $table Table name present in TCA
632     * @param array $row Record from $table
633     * @throws \RuntimeException
634     * @return string Field value
635     */
636    public static function getTCAtypeValue($table, $row)
637    {
638        $typeNum = 0;
639        if ($GLOBALS['TCA'][$table]) {
640            $field = $GLOBALS['TCA'][$table]['ctrl']['type'];
641            if (strpos($field, ':') !== false) {
642                [$pointerField, $foreignTableTypeField] = explode(':', $field);
643                // Get field value from database if field is not in the $row array
644                if (!isset($row[$pointerField])) {
645                    $localRow = self::getRecord($table, $row['uid'], $pointerField);
646                    $foreignUid = $localRow[$pointerField];
647                } else {
648                    $foreignUid = $row[$pointerField];
649                }
650                if ($foreignUid) {
651                    $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
652                    $relationType = $fieldConfig['type'];
653                    if ($relationType === 'select') {
654                        $foreignTable = $fieldConfig['foreign_table'];
655                    } elseif ($relationType === 'group') {
656                        $allowedTables = explode(',', $fieldConfig['allowed']);
657                        $foreignTable = $allowedTables[0];
658                    } else {
659                        throw new \RuntimeException(
660                            'TCA foreign field pointer fields are only allowed to be used with group or select field types.',
661                            1325862240
662                        );
663                    }
664                    $foreignRow = self::getRecord($foreignTable, $foreignUid, $foreignTableTypeField);
665                    if ($foreignRow[$foreignTableTypeField]) {
666                        $typeNum = $foreignRow[$foreignTableTypeField];
667                    }
668                }
669            } else {
670                $typeNum = $row[$field];
671            }
672            // If that value is an empty string, set it to "0" (zero)
673            if (empty($typeNum)) {
674                $typeNum = 0;
675            }
676        }
677        // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
678        if (!isset($GLOBALS['TCA'][$table]['types'][$typeNum]) || !$GLOBALS['TCA'][$table]['types'][$typeNum]) {
679            $typeNum = isset($GLOBALS['TCA'][$table]['types']['0']) ? 0 : 1;
680        }
681        // Force to string. Necessary for eg '-1' to be recognized as a type value.
682        $typeNum = (string)$typeNum;
683        return $typeNum;
684    }
685
686    /*******************************************
687     *
688     * TypoScript related
689     *
690     *******************************************/
691    /**
692     * Returns the Page TSconfig for page with id, $id
693     *
694     * @param int $id Page uid for which to create Page TSconfig
695     * @return array Page TSconfig
696     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
697     */
698    public static function getPagesTSconfig($id)
699    {
700        $id = (int)$id;
701
702        $cache = self::getRuntimeCache();
703        $pagesTsConfigIdToHash = $cache->get('pagesTsConfigIdToHash' . $id);
704        if ($pagesTsConfigIdToHash !== false) {
705            return $cache->get('pagesTsConfigHashToContent' . $pagesTsConfigIdToHash);
706        }
707
708        $rootLine = self::BEgetRootLine($id, '', true);
709        // Order correctly
710        ksort($rootLine);
711
712        try {
713            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id);
714        } catch (SiteNotFoundException $exception) {
715            $site = null;
716        }
717
718        // Load PageTS from all pages of the rootLine
719        $pageTs = GeneralUtility::makeInstance(PageTsConfigLoader::class)->load($rootLine);
720
721        // Parse the PageTS into an array, also applying conditions
722        $parser = GeneralUtility::makeInstance(
723            PageTsConfigParser::class,
724            GeneralUtility::makeInstance(TypoScriptParser::class),
725            GeneralUtility::makeInstance(CacheManager::class)->getCache('hash')
726        );
727        $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, null, $id, $rootLine);
728        $tsConfig = $parser->parse($pageTs, $matcher, $site);
729        $cacheHash = md5((string)json_encode($tsConfig));
730
731        // Get User TSconfig overlay, if no backend user is logged-in, this needs to be checked as well
732        if (static::getBackendUserAuthentication()) {
733            $userTSconfig = static::getBackendUserAuthentication()->getTSConfig() ?? [];
734        } else {
735            $userTSconfig = [];
736        }
737
738        if (is_array($userTSconfig['page.'] ?? null)) {
739            // Override page TSconfig with user TSconfig
740            ArrayUtility::mergeRecursiveWithOverrule($tsConfig, $userTSconfig['page.']);
741            $cacheHash .= '_user' . static::getBackendUserAuthentication()->user['uid'];
742        }
743
744        // Many pages end up with the same ts config. To reduce memory usage, the cache
745        // entries are a linked list: One or more pids point to content hashes which then
746        // contain the cached content.
747        $cache->set('pagesTsConfigHashToContent' . $cacheHash, $tsConfig, ['pagesTsConfig']);
748        $cache->set('pagesTsConfigIdToHash' . $id, $cacheHash, ['pagesTsConfig']);
749
750        return $tsConfig;
751    }
752
753    /**
754     * Returns the non-parsed Page TSconfig for page with id, $id
755     *
756     * @param int $id Page uid for which to create Page TSconfig
757     * @param array $rootLine If $rootLine is an array, that is used as rootline, otherwise rootline is just calculated
758     * @return array Non-parsed Page TSconfig
759     */
760    public static function getRawPagesTSconfig($id, array $rootLine = null)
761    {
762        trigger_error('BackendUtility::getRawPagesTSconfig will be removed in TYPO3 v11.0. Use PageTsConfigLoader instead.', E_USER_DEPRECATED);
763        if (!is_array($rootLine)) {
764            $rootLine = self::BEgetRootLine($id, '', true);
765        }
766
767        // Order correctly
768        ksort($rootLine);
769        $tsDataArray = [];
770        // Setting default configuration
771        $tsDataArray['defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
772        foreach ($rootLine as $k => $v) {
773            if (trim($v['tsconfig_includes'])) {
774                $includeTsConfigFileList = GeneralUtility::trimExplode(',', $v['tsconfig_includes'], true);
775                // Traversing list
776                foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
777                    if (strpos($includeTsConfigFile, 'EXT:') === 0) {
778                        [$includeTsConfigFileExtensionKey, $includeTsConfigFilename] = explode(
779                            '/',
780                            substr($includeTsConfigFile, 4),
781                            2
782                        );
783                        if ((string)$includeTsConfigFileExtensionKey !== ''
784                            && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
785                            && (string)$includeTsConfigFilename !== ''
786                        ) {
787                            $extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey);
788                            $includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename);
789                            if (strpos($includeTsConfigFileAndPath, $extensionPath) === 0 && file_exists($includeTsConfigFileAndPath)) {
790                                $tsDataArray['uid_' . $v['uid'] . '_static_' . $key] = file_get_contents($includeTsConfigFileAndPath);
791                            }
792                        }
793                    }
794                }
795            }
796            $tsDataArray['uid_' . $v['uid']] = $v['TSconfig'];
797        }
798
799        $eventDispatcher = GeneralUtility::getContainer()->get(EventDispatcherInterface::class);
800        $event = $eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent($tsDataArray, $rootLine));
801        return TypoScriptParser::checkIncludeLines_array($event->getTsConfig());
802    }
803
804    /*******************************************
805     *
806     * Users / Groups related
807     *
808     *******************************************/
809    /**
810     * Returns an array with be_users records of all user NOT DELETED sorted by their username
811     * Keys in the array is the be_users uid
812     *
813     * @param string $fields Optional $fields list (default: username,usergroup,usergroup_cached_list,uid) can be used to set the selected fields
814     * @param string $where Optional $where clause (fx. "AND username='pete'") can be used to limit query
815     * @return array
816     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
817     */
818    public static function getUserNames($fields = 'username,usergroup,usergroup_cached_list,uid', $where = '')
819    {
820        return self::getRecordsSortedByTitle(
821            GeneralUtility::trimExplode(',', $fields, true),
822            'be_users',
823            'username',
824            'AND pid=0 ' . $where
825        );
826    }
827
828    /**
829     * Returns an array with be_groups records (title, uid) of all groups NOT DELETED sorted by their title
830     *
831     * @param string $fields Field list
832     * @param string $where WHERE clause
833     * @return array
834     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
835     */
836    public static function getGroupNames($fields = 'title,uid', $where = '')
837    {
838        return self::getRecordsSortedByTitle(
839            GeneralUtility::trimExplode(',', $fields, true),
840            'be_groups',
841            'title',
842            'AND pid=0 ' . $where
843        );
844    }
845
846    /**
847     * Returns an array of all non-deleted records of a table sorted by a given title field.
848     * The value of the title field will be replaced by the return value
849     * of self::getRecordTitle() before the sorting is performed.
850     *
851     * @param array $fields Fields to select
852     * @param string $table Table name
853     * @param string $titleField Field that will contain the record title
854     * @param string $where Additional where clause
855     * @return array Array of sorted records
856     */
857    protected static function getRecordsSortedByTitle(array $fields, $table, $titleField, $where = '')
858    {
859        $fieldsIndex = array_flip($fields);
860        // Make sure the titleField is amongst the fields when getting sorted
861        $fieldsIndex[$titleField] = 1;
862
863        $result = [];
864
865        $queryBuilder = static::getQueryBuilderForTable($table);
866        $queryBuilder->getRestrictions()
867            ->removeAll()
868            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
869
870        $res = $queryBuilder
871            ->select('*')
872            ->from($table)
873            ->where(QueryHelper::stripLogicalOperatorPrefix($where))
874            ->execute();
875
876        while ($record = $res->fetch()) {
877            // store the uid, because it might be unset if it's not among the requested $fields
878            $recordId = $record['uid'];
879            $record[$titleField] = self::getRecordTitle($table, $record);
880
881            // include only the requested fields in the result
882            $result[$recordId] = array_intersect_key($record, $fieldsIndex);
883        }
884
885        // sort records by $sortField. This is not done in the query because the title might have been overwritten by
886        // self::getRecordTitle();
887        return ArrayUtility::sortArraysByKey($result, $titleField);
888    }
889
890    /**
891     * Returns the array $usernames with the names of all users NOT IN $groupArray changed to the uid (hides the usernames!).
892     * If $excludeBlindedFlag is set, then these records are unset from the array $usernames
893     * Takes $usernames (array made by \TYPO3\CMS\Backend\Utility\BackendUtility::getUserNames()) and a $groupArray (array with the groups a certain user is member of) as input
894     *
895     * @param array $usernames User names
896     * @param array $groupArray Group names
897     * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
898     * @return array User names, blinded
899     * @internal
900     */
901    public static function blindUserNames($usernames, $groupArray, $excludeBlindedFlag = false)
902    {
903        if (is_array($usernames) && is_array($groupArray)) {
904            foreach ($usernames as $uid => $row) {
905                $userN = $uid;
906                $set = 0;
907                if ($row['uid'] != static::getBackendUserAuthentication()->user['uid']) {
908                    foreach ($groupArray as $v) {
909                        if ($v && GeneralUtility::inList($row['usergroup_cached_list'], $v)) {
910                            $userN = $row['username'];
911                            $set = 1;
912                        }
913                    }
914                } else {
915                    $userN = $row['username'];
916                    $set = 1;
917                }
918                $usernames[$uid]['username'] = $userN;
919                if ($excludeBlindedFlag && !$set) {
920                    unset($usernames[$uid]);
921                }
922            }
923        }
924        return $usernames;
925    }
926
927    /**
928     * Corresponds to blindUserNames but works for groups instead
929     *
930     * @param array $groups Group names
931     * @param array $groupArray Group names (reference)
932     * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
933     * @return array
934     * @internal
935     */
936    public static function blindGroupNames($groups, $groupArray, $excludeBlindedFlag = false)
937    {
938        if (is_array($groups) && is_array($groupArray)) {
939            foreach ($groups as $uid => $row) {
940                $groupN = $uid;
941                $set = 0;
942                if (in_array($uid, $groupArray, false)) {
943                    $groupN = $row['title'];
944                    $set = 1;
945                }
946                $groups[$uid]['title'] = $groupN;
947                if ($excludeBlindedFlag && !$set) {
948                    unset($groups[$uid]);
949                }
950            }
951        }
952        return $groups;
953    }
954
955    /*******************************************
956     *
957     * Output related
958     *
959     *******************************************/
960    /**
961     * Returns the difference in days between input $tstamp and $EXEC_TIME
962     *
963     * @param int $tstamp Time stamp, seconds
964     * @return int
965     */
966    public static function daysUntil($tstamp)
967    {
968        $delta_t = $tstamp - $GLOBALS['EXEC_TIME'];
969        return ceil($delta_t / (3600 * 24));
970    }
971
972    /**
973     * Returns $tstamp formatted as "ddmmyy" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'])
974     *
975     * @param int $tstamp Time stamp, seconds
976     * @return string Formatted time
977     */
978    public static function date($tstamp)
979    {
980        return date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$tstamp);
981    }
982
983    /**
984     * Returns $tstamp formatted as "ddmmyy hhmm" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] AND $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'])
985     *
986     * @param int $value Time stamp, seconds
987     * @return string Formatted time
988     */
989    public static function datetime($value)
990    {
991        return date(
992            $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
993            $value
994        );
995    }
996
997    /**
998     * Returns $value (in seconds) formatted as hh:mm:ss
999     * For instance $value = 3600 + 60*2 + 3 should return "01:02:03"
1000     *
1001     * @param int $value Time stamp, seconds
1002     * @param bool $withSeconds Output hh:mm:ss. If FALSE: hh:mm
1003     * @return string Formatted time
1004     */
1005    public static function time($value, $withSeconds = true)
1006    {
1007        return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value);
1008    }
1009
1010    /**
1011     * Returns the "age" in minutes / hours / days / years of the number of $seconds inputted.
1012     *
1013     * @param int $seconds Seconds could be the difference of a certain timestamp and time()
1014     * @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")
1015     * @return string Formatted time
1016     */
1017    public static function calcAge($seconds, $labels = 'min|hrs|days|yrs|min|hour|day|year')
1018    {
1019        $labelArr = GeneralUtility::trimExplode('|', $labels, true);
1020        $absSeconds = abs($seconds);
1021        $sign = $seconds < 0 ? -1 : 1;
1022        if ($absSeconds < 3600) {
1023            $val = round($absSeconds / 60);
1024            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[4] : $labelArr[0]);
1025        } elseif ($absSeconds < 24 * 3600) {
1026            $val = round($absSeconds / 3600);
1027            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[5] : $labelArr[1]);
1028        } elseif ($absSeconds < 365 * 24 * 3600) {
1029            $val = round($absSeconds / (24 * 3600));
1030            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[6] : $labelArr[2]);
1031        } else {
1032            $val = round($absSeconds / (365 * 24 * 3600));
1033            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[7] : $labelArr[3]);
1034        }
1035        return $seconds;
1036    }
1037
1038    /**
1039     * Returns a formatted timestamp if $tstamp is set.
1040     * The date/datetime will be followed by the age in parenthesis.
1041     *
1042     * @param int $tstamp Time stamp, seconds
1043     * @param int $prefix 1/-1 depending on polarity of age.
1044     * @param string $date $date=="date" will yield "dd:mm:yy" formatting, otherwise "dd:mm:yy hh:mm
1045     * @return string
1046     */
1047    public static function dateTimeAge($tstamp, $prefix = 1, $date = '')
1048    {
1049        if (!$tstamp) {
1050            return '';
1051        }
1052        $label = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
1053        $age = ' (' . self::calcAge($prefix * ($GLOBALS['EXEC_TIME'] - $tstamp), $label) . ')';
1054        return ($date === 'date' ? self::date($tstamp) : self::datetime($tstamp)) . $age;
1055    }
1056
1057    /**
1058     * Resolves file references for a given record.
1059     *
1060     * @param string $tableName Name of the table of the record
1061     * @param string $fieldName Name of the field of the record
1062     * @param array $element Record data
1063     * @param int|null $workspaceId Workspace to fetch data for
1064     * @return \TYPO3\CMS\Core\Resource\FileReference[]|null
1065     */
1066    public static function resolveFileReferences($tableName, $fieldName, $element, $workspaceId = null)
1067    {
1068        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1069            return null;
1070        }
1071        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1072        if (empty($configuration['type']) || $configuration['type'] !== 'inline'
1073            || empty($configuration['foreign_table']) || $configuration['foreign_table'] !== 'sys_file_reference'
1074        ) {
1075            return null;
1076        }
1077
1078        $fileReferences = [];
1079        /** @var RelationHandler $relationHandler */
1080        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1081        if ($workspaceId !== null) {
1082            $relationHandler->setWorkspaceId($workspaceId);
1083        }
1084        $relationHandler->start(
1085            $element[$fieldName],
1086            $configuration['foreign_table'],
1087            $configuration['MM'] ?? '',
1088            $element['uid'],
1089            $tableName,
1090            $configuration
1091        );
1092        $relationHandler->processDeletePlaceholder();
1093        $referenceUids = $relationHandler->tableArray[$configuration['foreign_table']];
1094
1095        foreach ($referenceUids as $referenceUid) {
1096            try {
1097                $fileReference = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject(
1098                    $referenceUid,
1099                    [],
1100                    $workspaceId === 0
1101                );
1102                $fileReferences[$fileReference->getUid()] = $fileReference;
1103            } catch (FileDoesNotExistException $e) {
1104                /**
1105                 * We just catch the exception here
1106                 * Reasoning: There is nothing an editor or even admin could do
1107                 */
1108            } catch (\InvalidArgumentException $e) {
1109                /**
1110                 * The storage does not exist anymore
1111                 * Log the exception message for admins as they maybe can restore the storage
1112                 */
1113                self::getLogger()->error($e->getMessage(), ['table' => $tableName, 'fieldName' => $fieldName, 'referenceUid' => $referenceUid, 'exception' => $e]);
1114            }
1115        }
1116
1117        return $fileReferences;
1118    }
1119
1120    /**
1121     * Returns a linked image-tag for thumbnail(s)/fileicons/truetype-font-previews from a database row with sys_file_references
1122     * All $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] extension are made to thumbnails + ttf file (renders font-example)
1123     * Thumbnails are linked to ShowItemController (/thumbnails route)
1124     *
1125     * @param array $row Row is the database row from the table, $table.
1126     * @param string $table Table name for $row (present in TCA)
1127     * @param string $field Field is pointing to the connecting field of sys_file_references
1128     * @param string $backPath Back path prefix for image tag src="" field
1129     * @param string $thumbScript UNUSED since FAL
1130     * @param string $uploaddir UNUSED since FAL
1131     * @param int $abs UNUSED
1132     * @param string $tparams Optional: $tparams is additional attributes for the image tags
1133     * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default.
1134     * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup
1135     * @return string Thumbnail image tag.
1136     */
1137    public static function thumbCode(
1138        $row,
1139        $table,
1140        $field,
1141        $backPath = '',
1142        $thumbScript = '',
1143        $uploaddir = null,
1144        $abs = 0,
1145        $tparams = '',
1146        $size = '',
1147        $linkInfoPopup = true
1148    ) {
1149        $size = (int)(trim((string)$size) ?: 64);
1150        $targetDimension = new ImageDimension($size, $size);
1151        $thumbData = '';
1152        $fileReferences = static::resolveFileReferences($table, $field, $row);
1153        // FAL references
1154        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1155        if ($fileReferences !== null) {
1156            foreach ($fileReferences as $fileReferenceObject) {
1157                // Do not show previews of hidden references
1158                if ($fileReferenceObject->getProperty('hidden')) {
1159                    continue;
1160                }
1161                $fileObject = $fileReferenceObject->getOriginalFile();
1162
1163                if ($fileObject->isMissing()) {
1164                    $thumbData .= '<span class="label label-danger">'
1165                        . htmlspecialchars(
1166                            static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')
1167                        )
1168                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1169                    continue;
1170                }
1171
1172                // Preview web image or media elements
1173                if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
1174                    && $fileReferenceObject->getOriginalFile()->isImage()
1175                ) {
1176                    $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
1177                    $cropArea = $cropVariantCollection->getCropArea();
1178                    $taskType = ProcessedFile::CONTEXT_IMAGEPREVIEW;
1179                    $processingConfiguration = [
1180                        'width' => $targetDimension->getWidth(),
1181                        'height' => $targetDimension->getHeight(),
1182                    ];
1183                    if (!$cropArea->isEmpty()) {
1184                        $taskType = ProcessedFile::CONTEXT_IMAGECROPSCALEMASK;
1185                        $processingConfiguration = [
1186                            'maxWidth' => $targetDimension->getWidth(),
1187                            'maxHeight' => $targetDimension->getHeight(),
1188                            'crop' => $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
1189                        ];
1190                    }
1191                    $processedImage = $fileObject->process($taskType, $processingConfiguration);
1192                    $attributes = [
1193                        'src' => $processedImage->getPublicUrl(true),
1194                        'width' => $processedImage->getProperty('width'),
1195                        'height' => $processedImage->getProperty('height'),
1196                        'alt' => $fileReferenceObject->getName(),
1197                    ];
1198                    $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . $tparams . '/>';
1199                } else {
1200                    // Icon
1201                    $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1202                        . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1203                        . '</span>';
1204                }
1205                if ($linkInfoPopup) {
1206                    // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
1207                    $attributes = GeneralUtility::implodeAttributes([
1208                        'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
1209                        'data-dispatch-args-list' => '_FILE,' . (int)$fileObject->getUid(),
1210                    ], true);
1211                    $thumbData .= '<a href="#" ' . $attributes . '>' . $imgTag . '</a> ';
1212                } else {
1213                    $thumbData .= $imgTag;
1214                }
1215            }
1216        }
1217        return $thumbData;
1218    }
1219
1220    /**
1221     * @param int $fileId
1222     * @param array $configuration
1223     * @return string
1224     */
1225    public static function getThumbnailUrl(int $fileId, array $configuration): string
1226    {
1227        $taskType = $configuration['_context'] ?? ProcessedFile::CONTEXT_IMAGEPREVIEW;
1228        unset($configuration['_context']);
1229
1230        return GeneralUtility::makeInstance(ResourceFactory::class)
1231                ->getFileObject($fileId)
1232                ->process($taskType, $configuration)
1233                ->getPublicUrl(true);
1234    }
1235
1236    /**
1237     * Returns title-attribute information for a page-record informing about id, doktype, hidden, starttime, endtime, fe_group etc.
1238     *
1239     * @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)
1240     * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4)
1241     * @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
1242     * @return string
1243     */
1244    public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true)
1245    {
1246        $lang = static::getLanguageService();
1247        $parts = [];
1248        $parts[] = 'id=' . $row['uid'];
1249        if ($row['uid'] === 0) {
1250            $out = htmlspecialchars($parts[0]);
1251            return $includeAttrib ? 'title="' . $out . '"' : $out;
1252        }
1253        switch (VersionState::cast($row['t3ver_state'])) {
1254            case new VersionState(VersionState::NEW_PLACEHOLDER):
1255                $parts[] = 'PLH WSID#' . $row['t3ver_wsid'];
1256                break;
1257            case new VersionState(VersionState::DELETE_PLACEHOLDER):
1258                $parts[] = 'Deleted element!';
1259                break;
1260            case new VersionState(VersionState::MOVE_PLACEHOLDER):
1261                $parts[] = 'OLD LOCATION (Move Placeholder) WSID#' . $row['t3ver_wsid'];
1262                break;
1263            case new VersionState(VersionState::MOVE_POINTER):
1264                $parts[] = 'NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1265                break;
1266            case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1267                $parts[] = 'New element!';
1268                break;
1269        }
1270        if ($row['doktype'] == PageRepository::DOKTYPE_LINK) {
1271            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label']) . ' ' . $row['url'];
1272        } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1273            if ($perms_clause) {
1274                $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20);
1275            } else {
1276                $row['shortcut'] = (int)$row['shortcut'];
1277                $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title');
1278                $label = $lRec['title'] . ' (id=' . $row['shortcut'] . ')';
1279            }
1280            if ($row['shortcut_mode'] != PageRepository::SHORTCUT_MODE_NONE) {
1281                $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' '
1282                    . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode']));
1283            }
1284            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . $label;
1285        } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) {
1286            if ((int)$row['mount_pid'] > 0) {
1287                if ($perms_clause) {
1288                    $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20);
1289                } else {
1290                    $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title');
1291                    $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')';
1292                }
1293                $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label;
1294                if ($row['mount_pid_ol']) {
1295                    $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']);
1296                }
1297            } else {
1298                $parts[] = $lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:no_mount_pid');
1299            }
1300        }
1301        if ($row['nav_hide']) {
1302            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide');
1303        }
1304        if ($row['hidden']) {
1305            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden');
1306        }
1307        if ($row['starttime']) {
1308            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label'])
1309                . ' ' . self::dateTimeAge($row['starttime'], -1, 'date');
1310        }
1311        if ($row['endtime']) {
1312            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' '
1313                . self::dateTimeAge($row['endtime'], -1, 'date');
1314        }
1315        if ($row['fe_group']) {
1316            $fe_groups = [];
1317            foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) {
1318                if ($fe_group < 0) {
1319                    $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', (string)$fe_group));
1320                } else {
1321                    $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title');
1322                    $fe_groups[] = $lRec['title'];
1323                }
1324            }
1325            $label = implode(', ', $fe_groups);
1326            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label;
1327        }
1328        $out = htmlspecialchars(implode(' - ', $parts));
1329        return $includeAttrib ? 'title="' . $out . '"' : $out;
1330    }
1331
1332    /**
1333     * Returns the combined markup for Bootstraps tooltips
1334     *
1335     * @param array $row
1336     * @param string $table
1337     * @return string
1338     */
1339    public static function getRecordToolTip(array $row, $table = 'pages')
1340    {
1341        $toolTipText = self::getRecordIconAltText($row, $table);
1342        $toolTipCode = 'data-toggle="tooltip" data-title=" '
1343            . str_replace(' - ', '<br>', $toolTipText)
1344            . '" data-html="true" data-placement="right"';
1345        return $toolTipCode;
1346    }
1347
1348    /**
1349     * Returns title-attribute information for ANY record (from a table defined in TCA of course)
1350     * 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.
1351     * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page.
1352     *
1353     * @param array $row Table row; $row is a row from the table, $table
1354     * @param string $table Table name
1355     * @return string
1356     */
1357    public static function getRecordIconAltText($row, $table = 'pages')
1358    {
1359        if ($table === 'pages') {
1360            $out = self::titleAttribForPages($row, '', false);
1361        } else {
1362            $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'])) ? $row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] . ' ' : '';
1363            $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
1364            // Uid is added
1365            $out .= 'id=' . $row['uid'];
1366            if (static::isTableWorkspaceEnabled($table)) {
1367                switch (VersionState::cast($row['t3ver_state'])) {
1368                    case new VersionState(VersionState::NEW_PLACEHOLDER):
1369                        $out .= ' - PLH WSID#' . $row['t3ver_wsid'];
1370                        break;
1371                    case new VersionState(VersionState::DELETE_PLACEHOLDER):
1372                        $out .= ' - Deleted element!';
1373                        break;
1374                    case new VersionState(VersionState::MOVE_PLACEHOLDER):
1375                        $out .= ' - OLD LOCATION (Move Placeholder) WSID#' . $row['t3ver_wsid'];
1376                        break;
1377                    case new VersionState(VersionState::MOVE_POINTER):
1378                        $out .= ' - NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1379                        break;
1380                    case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1381                        $out .= ' - New element!';
1382                        break;
1383                }
1384            }
1385            // Hidden
1386            $lang = static::getLanguageService();
1387            if ($ctrl['disabled']) {
1388                $out .= $row[$ctrl['disabled']] ? ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') : '';
1389            }
1390            if ($ctrl['starttime']) {
1391                if ($row[$ctrl['starttime']] > $GLOBALS['EXEC_TIME']) {
1392                    $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') . ')';
1393                }
1394            }
1395            if ($row[$ctrl['endtime']]) {
1396                $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') . ')';
1397            }
1398        }
1399        return htmlspecialchars($out);
1400    }
1401
1402    /**
1403     * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key
1404     *
1405     * @param string $table Table name, present in $GLOBALS['TCA']
1406     * @param string $col Field name, present in $GLOBALS['TCA']
1407     * @param string $key items-array value to match
1408     * @return string Label for item entry
1409     */
1410    public static function getLabelFromItemlist($table, $col, $key)
1411    {
1412        // Check, if there is an "items" array:
1413        if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) {
1414            // Traverse the items-array...
1415            foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) {
1416                // ... and return the first found label where the value was equal to $key
1417                if ((string)$v[1] === (string)$key) {
1418                    return $v[0];
1419                }
1420            }
1421        }
1422        return '';
1423    }
1424
1425    /**
1426     * Return the label of a field by additionally checking TsConfig values
1427     *
1428     * @param int $pageId Page id
1429     * @param string $table Table name
1430     * @param string $column Field Name
1431     * @param string $key item value
1432     * @return string Label for item entry
1433     */
1434    public static function getLabelFromItemListMerged($pageId, $table, $column, $key)
1435    {
1436        $pageTsConfig = static::getPagesTSconfig($pageId);
1437        $label = '';
1438        if (isset($pageTsConfig['TCEFORM.'])
1439            && \is_array($pageTsConfig['TCEFORM.'])
1440            && \is_array($pageTsConfig['TCEFORM.'][$table . '.'])
1441            && \is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.'])
1442        ) {
1443            if (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'])
1444                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key])
1445            ) {
1446                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key];
1447            } elseif (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'])
1448                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key])
1449            ) {
1450                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key];
1451            }
1452        }
1453        if (empty($label)) {
1454            $tcaValue = self::getLabelFromItemlist($table, $column, $key);
1455            if (!empty($tcaValue)) {
1456                $label = $tcaValue;
1457            }
1458        }
1459        return $label;
1460    }
1461
1462    /**
1463     * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma.
1464     * NOTE: this does not take itemsProcFunc into account
1465     *
1466     * @param string $table Table name, present in TCA
1467     * @param string $column Field name
1468     * @param string $keyList Key or comma-separated list of keys.
1469     * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>)
1470     * @return string Comma-separated list of localized labels
1471     */
1472    public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = [])
1473    {
1474        // Check if there is an "items" array
1475        if (
1476            !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1477            || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1478            || $keyList === ''
1479        ) {
1480            return '';
1481        }
1482
1483        $keys = GeneralUtility::trimExplode(',', $keyList, true);
1484        $labels = [];
1485        // Loop on all selected values
1486        foreach ($keys as $key) {
1487            $label = null;
1488            if ($columnTsConfig) {
1489                // Check if label has been defined or redefined via pageTsConfig
1490                if (isset($columnTsConfig['addItems.'][$key])) {
1491                    $label = $columnTsConfig['addItems.'][$key];
1492                } elseif (isset($columnTsConfig['altLabels.'][$key])) {
1493                    $label = $columnTsConfig['altLabels.'][$key];
1494                }
1495            }
1496            if ($label === null) {
1497                // Otherwise lookup the label in TCA items list
1498                foreach ($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'] as $itemConfiguration) {
1499                    [$currentLabel, $currentKey] = $itemConfiguration;
1500                    if ((string)$key === (string)$currentKey) {
1501                        $label = $currentLabel;
1502                        break;
1503                    }
1504                }
1505            }
1506            if ($label !== null) {
1507                $labels[] = static::getLanguageService()->sL($label);
1508            }
1509        }
1510        return implode(', ', $labels);
1511    }
1512
1513    /**
1514     * Returns the label-value for fieldname $col in table, $table
1515     * 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>'
1516     *
1517     * @param string $table Table name, present in $GLOBALS['TCA']
1518     * @param string $col Field name
1519     * @return string or NULL if $col is not found in the TCA table
1520     */
1521    public static function getItemLabel($table, $col)
1522    {
1523        // Check if column exists
1524        if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1525            return $GLOBALS['TCA'][$table]['columns'][$col]['label'];
1526        }
1527
1528        return null;
1529    }
1530
1531    /**
1532     * Returns the "title"-value in record, $row, from table, $table
1533     * The field(s) from which the value is taken is determined by the "ctrl"-entries 'label', 'label_alt' and 'label_alt_force'
1534     *
1535     * @param string $table Table name, present in TCA
1536     * @param array $row Row from table
1537     * @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
1538     * @param bool $forceResult If set, the function always returns an output. If no value is found for the title, '[No title]' is returned (localized).
1539     * @return string
1540     */
1541    public static function getRecordTitle($table, $row, $prep = false, $forceResult = true)
1542    {
1543        $params = [];
1544        $recordTitle = '';
1545        if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table])) {
1546            // If configured, call userFunc
1547            if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'])) {
1548                $params['table'] = $table;
1549                $params['row'] = $row;
1550                $params['title'] = '';
1551                $params['options'] = $GLOBALS['TCA'][$table]['ctrl']['label_userFunc_options'] ?? [];
1552
1553                // Create NULL-reference
1554                $null = null;
1555                GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'], $params, $null);
1556                $recordTitle = $params['title'];
1557            } else {
1558                // No userFunc: Build label
1559                $recordTitle = self::getProcessedValue(
1560                    $table,
1561                    $GLOBALS['TCA'][$table]['ctrl']['label'],
1562                    $row[$GLOBALS['TCA'][$table]['ctrl']['label']],
1563                    0,
1564                    false,
1565                    false,
1566                    $row['uid'],
1567                    $forceResult
1568                ) ?? '';
1569                if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])
1570                    && (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) || (string)$recordTitle === '')
1571                ) {
1572                    $altFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
1573                    $tA = [];
1574                    if (!empty($recordTitle)) {
1575                        $tA[] = $recordTitle;
1576                    }
1577                    foreach ($altFields as $fN) {
1578                        $recordTitle = trim(strip_tags($row[$fN]));
1579                        if ((string)$recordTitle !== '') {
1580                            $recordTitle = self::getProcessedValue($table, $fN, $recordTitle, 0, false, false, $row['uid']);
1581                            if (!$GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1582                                break;
1583                            }
1584                            $tA[] = $recordTitle;
1585                        }
1586                    }
1587                    if ($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1588                        $recordTitle = implode(', ', $tA);
1589                    }
1590                }
1591            }
1592            // If the current result is empty, set it to '[No title]' (localized) and prepare for output if requested
1593            if ($prep || $forceResult) {
1594                if ($prep) {
1595                    $recordTitle = self::getRecordTitlePrep($recordTitle);
1596                }
1597                if (trim($recordTitle) === '') {
1598                    $recordTitle = self::getNoRecordTitle($prep);
1599                }
1600            }
1601        }
1602
1603        return $recordTitle;
1604    }
1605
1606    /**
1607     * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>,
1608     * which offers a tooltip with the original title when moving mouse over it.
1609     *
1610     * @param string $title The title string to be cropped
1611     * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
1612     * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
1613     */
1614    public static function getRecordTitlePrep($title, $titleLength = 0)
1615    {
1616        // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
1617        if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
1618            $titleLength = static::getBackendUserAuthentication()->uc['titleLen'];
1619        }
1620        $titleOrig = htmlspecialchars($title);
1621        $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
1622        // If title was cropped, offer a tooltip:
1623        if ($titleOrig != $title) {
1624            $title = '<span title="' . $titleOrig . '">' . $title . '</span>';
1625        }
1626        return $title;
1627    }
1628
1629    /**
1630     * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE.
1631     *
1632     * @param bool $prep Wrap result in <em>|</em>
1633     * @return string Localized [No title] string
1634     */
1635    public static function getNoRecordTitle($prep = false)
1636    {
1637        $noTitle = '[' .
1638            htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title'))
1639            . ']';
1640        if ($prep) {
1641            $noTitle = '<em>' . $noTitle . '</em>';
1642        }
1643        return $noTitle;
1644    }
1645
1646    /**
1647     * Returns a human readable output of a value from a record
1648     * 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.
1649     * $table/$col is tablename and fieldname
1650     * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant)
1651     *
1652     * @param string $table Table name, present in TCA
1653     * @param string $col Field name, present in TCA
1654     * @param string $value The value of that field from a selected record
1655     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1656     * @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")
1657     * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown.
1658     * @param int $uid Uid of the current record
1659     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1660     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1661     * @throws \InvalidArgumentException
1662     * @return string|null
1663     */
1664    public static function getProcessedValue(
1665        $table,
1666        $col,
1667        $value,
1668        $fixed_lgd_chars = 0,
1669        $defaultPassthrough = false,
1670        $noRecordLookup = false,
1671        $uid = 0,
1672        $forceResult = true,
1673        $pid = 0
1674    ) {
1675        if ($col === 'uid') {
1676            // uid is not in TCA-array
1677            return $value;
1678        }
1679        // Check if table and field is configured
1680        if (!isset($GLOBALS['TCA'][$table]['columns'][$col]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1681            return null;
1682        }
1683        // Depending on the fields configuration, make a meaningful output value.
1684        $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? [];
1685        /*****************
1686         *HOOK: pre-processing the human readable output from a record
1687         ****************/
1688        $referenceObject = new \stdClass();
1689        $referenceObject->table = $table;
1690        $referenceObject->fieldName = $col;
1691        $referenceObject->uid = $uid;
1692        $referenceObject->value = &$value;
1693        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) {
1694            GeneralUtility::callUserFunction($_funcRef, $theColConf, $referenceObject);
1695        }
1696
1697        $l = '';
1698        $lang = static::getLanguageService();
1699        switch ((string)($theColConf['type'] ?? '')) {
1700            case 'radio':
1701                $l = self::getLabelFromItemlist($table, $col, $value);
1702                $l = $lang->sL($l);
1703                break;
1704            case 'inline':
1705            case 'select':
1706                if (!empty($theColConf['MM'])) {
1707                    if ($uid) {
1708                        // Display the title of MM related records in lists
1709                        if ($noRecordLookup) {
1710                            $MMfields = [];
1711                            $MMfields[] = $theColConf['foreign_table'] . '.uid';
1712                        } else {
1713                            $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1714                            if (isset($GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'])) {
1715                                foreach (GeneralUtility::trimExplode(
1716                                    ',',
1717                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'],
1718                                    true
1719                                ) as $f) {
1720                                    $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1721                                }
1722                            }
1723                        }
1724                        /** @var RelationHandler $dbGroup */
1725                        $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1726                        $dbGroup->start(
1727                            $value,
1728                            $theColConf['foreign_table'],
1729                            $theColConf['MM'],
1730                            $uid,
1731                            $table,
1732                            $theColConf
1733                        );
1734                        $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1735                        if (is_array($selectUids) && !empty($selectUids)) {
1736                            $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1737                            $queryBuilder->getRestrictions()
1738                                ->removeAll()
1739                                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1740
1741                            $result = $queryBuilder
1742                                ->select('uid', ...$MMfields)
1743                                ->from($theColConf['foreign_table'])
1744                                ->where(
1745                                    $queryBuilder->expr()->in(
1746                                        'uid',
1747                                        $queryBuilder->createNamedParameter($selectUids, Connection::PARAM_INT_ARRAY)
1748                                    )
1749                                )
1750                                ->execute();
1751
1752                            $mmlA = [];
1753                            while ($MMrow = $result->fetch()) {
1754                                // Keep sorting of $selectUids
1755                                $selectedUid = array_search($MMrow['uid'], $selectUids);
1756                                $mmlA[$selectedUid] = $MMrow['uid'];
1757                                if (!$noRecordLookup) {
1758                                    $mmlA[$selectedUid] = static::getRecordTitle(
1759                                        $theColConf['foreign_table'],
1760                                        $MMrow,
1761                                        false,
1762                                        $forceResult
1763                                    );
1764                                }
1765                            }
1766
1767                            if (!empty($mmlA)) {
1768                                ksort($mmlA);
1769                                $l = implode('; ', $mmlA);
1770                            } else {
1771                                $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1772                            }
1773                        } else {
1774                            $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1775                        }
1776                    } else {
1777                        $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1778                    }
1779                } else {
1780                    $columnTsConfig = [];
1781                    if ($pid) {
1782                        $pageTsConfig = self::getPagesTSconfig($pid);
1783                        if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) {
1784                            $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'];
1785                        }
1786                    }
1787                    $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig);
1788                    if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) {
1789                        if ($noRecordLookup) {
1790                            $l = $value;
1791                        } else {
1792                            $rParts = [];
1793                            if ($uid && isset($theColConf['foreign_field']) && $theColConf['foreign_field'] !== '') {
1794                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1795                                $queryBuilder->getRestrictions()
1796                                    ->removeAll()
1797                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1798                                    ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace));
1799                                $constraints = [
1800                                    $queryBuilder->expr()->eq(
1801                                        $theColConf['foreign_field'],
1802                                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1803                                    )
1804                                ];
1805
1806                                if (!empty($theColConf['foreign_table_field'])) {
1807                                    $constraints[] = $queryBuilder->expr()->eq(
1808                                        $theColConf['foreign_table_field'],
1809                                        $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
1810                                    );
1811                                }
1812
1813                                // Add additional where clause if foreign_match_fields are defined
1814                                $foreignMatchFields = [];
1815                                if (is_array($theColConf['foreign_match_fields'])) {
1816                                    $foreignMatchFields = $theColConf['foreign_match_fields'];
1817                                }
1818
1819                                foreach ($foreignMatchFields as $matchField => $matchValue) {
1820                                    $constraints[] = $queryBuilder->expr()->eq(
1821                                        $matchField,
1822                                        $queryBuilder->createNamedParameter($matchValue)
1823                                    );
1824                                }
1825
1826                                $result = $queryBuilder
1827                                    ->select('*')
1828                                    ->from($theColConf['foreign_table'])
1829                                    ->where(...$constraints)
1830                                    ->execute();
1831
1832                                while ($record = $result->fetch()) {
1833                                    $rParts[] = $record['uid'];
1834                                }
1835                            }
1836                            if (empty($rParts)) {
1837                                $rParts = GeneralUtility::trimExplode(',', $value, true);
1838                            }
1839                            $lA = [];
1840                            foreach ($rParts as $rVal) {
1841                                $rVal = (int)$rVal;
1842                                $r = self::getRecordWSOL($theColConf['foreign_table'], $rVal);
1843                                if (is_array($r)) {
1844                                    $lA[] = $lang->sL($theColConf['foreign_table_prefix'])
1845                                        . self::getRecordTitle($theColConf['foreign_table'], $r, false, $forceResult);
1846                                } else {
1847                                    $lA[] = $rVal ? '[' . $rVal . '!]' : '';
1848                                }
1849                            }
1850                            $l = implode(', ', $lA);
1851                        }
1852                    }
1853                    if (empty($l) && !empty($value)) {
1854                        // Use plain database value when label is empty
1855                        $l = $value;
1856                    }
1857                }
1858                break;
1859            case 'group':
1860                // resolve the titles for DB records
1861                if (isset($theColConf['internal_type']) && $theColConf['internal_type'] === 'db') {
1862                    if (isset($theColConf['MM']) && $theColConf['MM']) {
1863                        if ($uid) {
1864                            // Display the title of MM related records in lists
1865                            if ($noRecordLookup) {
1866                                $MMfields = [];
1867                                $MMfields[] = $theColConf['foreign_table'] . '.uid';
1868                            } else {
1869                                $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1870                                $altLabelFields = explode(
1871                                    ',',
1872                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt']
1873                                );
1874                                foreach ($altLabelFields as $f) {
1875                                    $f = trim($f);
1876                                    if ($f !== '') {
1877                                        $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1878                                    }
1879                                }
1880                            }
1881                            /** @var RelationHandler $dbGroup */
1882                            $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1883                            $dbGroup->start(
1884                                $value,
1885                                $theColConf['foreign_table'],
1886                                $theColConf['MM'],
1887                                $uid,
1888                                $table,
1889                                $theColConf
1890                            );
1891                            $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1892                            if (!empty($selectUids) && is_array($selectUids)) {
1893                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1894                                $queryBuilder->getRestrictions()
1895                                    ->removeAll()
1896                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1897
1898                                $result = $queryBuilder
1899                                    ->select('uid', ...$MMfields)
1900                                    ->from($theColConf['foreign_table'])
1901                                    ->where(
1902                                        $queryBuilder->expr()->in(
1903                                            'uid',
1904                                            $queryBuilder->createNamedParameter(
1905                                                $selectUids,
1906                                                Connection::PARAM_INT_ARRAY
1907                                            )
1908                                        )
1909                                    )
1910                                    ->execute();
1911
1912                                $mmlA = [];
1913                                while ($MMrow = $result->fetch()) {
1914                                    // Keep sorting of $selectUids
1915                                    $selectedUid = array_search($MMrow['uid'], $selectUids);
1916                                    $mmlA[$selectedUid] = $MMrow['uid'];
1917                                    if (!$noRecordLookup) {
1918                                        $mmlA[$selectedUid] = static::getRecordTitle(
1919                                            $theColConf['foreign_table'],
1920                                            $MMrow,
1921                                            false,
1922                                            $forceResult
1923                                        );
1924                                    }
1925                                }
1926
1927                                if (!empty($mmlA)) {
1928                                    ksort($mmlA);
1929                                    $l = implode('; ', $mmlA);
1930                                } else {
1931                                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1932                                }
1933                            } else {
1934                                $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1935                            }
1936                        } else {
1937                            $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1938                        }
1939                    } else {
1940                        $finalValues = [];
1941                        $relationTableName = $theColConf['allowed'];
1942                        $explodedValues = GeneralUtility::trimExplode(',', $value, true);
1943
1944                        foreach ($explodedValues as $explodedValue) {
1945                            if (MathUtility::canBeInterpretedAsInteger($explodedValue)) {
1946                                $relationTableNameForField = $relationTableName;
1947                            } else {
1948                                [$relationTableNameForField, $explodedValue] = self::splitTable_Uid($explodedValue);
1949                            }
1950
1951                            $relationRecord = static::getRecordWSOL($relationTableNameForField, $explodedValue);
1952                            $finalValues[] = static::getRecordTitle($relationTableNameForField, $relationRecord);
1953                        }
1954                        $l = implode(', ', $finalValues);
1955                    }
1956                } else {
1957                    $l = implode(', ', GeneralUtility::trimExplode(',', $value, true));
1958                }
1959                break;
1960            case 'check':
1961                if (!is_array($theColConf['items'])) {
1962                    $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');
1963                } elseif (count($theColConf['items']) === 1) {
1964                    reset($theColConf['items']);
1965                    $invertStateDisplay = current($theColConf['items'])['invertStateDisplay'] ?? false;
1966                    if ($invertStateDisplay) {
1967                        $value = !$value;
1968                    }
1969                    $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');
1970                } else {
1971                    $lA = [];
1972                    foreach ($theColConf['items'] as $key => $val) {
1973                        if ($value & 2 ** $key) {
1974                            $lA[] = $lang->sL($val[0]);
1975                        }
1976                    }
1977                    $l = implode(', ', $lA);
1978                }
1979                break;
1980            case 'input':
1981                // Hide value 0 for dates, but show it for everything else
1982                // todo: phpstan states that $value always exists and is not nullable. At the moment, this is a false
1983                //       positive as null can be passed into this method via $value. As soon as more strict types are
1984                //       used, this isset check must be replaced with a more appropriate check.
1985                if (isset($value)) {
1986                    $dateTimeFormats = QueryHelper::getDateTimeFormats();
1987
1988                    if (GeneralUtility::inList($theColConf['eval'] ?? '', 'date')) {
1989                        // Handle native date field
1990                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') {
1991                            $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value);
1992                        } else {
1993                            $value = (int)$value;
1994                        }
1995                        if (!empty($value)) {
1996                            $ageSuffix = '';
1997                            $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
1998                            $ageDisplayKey = 'disableAgeDisplay';
1999
2000                            // generate age suffix as long as not explicitly suppressed
2001                            if (!isset($dateColumnConfiguration[$ageDisplayKey])
2002                                // non typesafe comparison on intention
2003                                || $dateColumnConfiguration[$ageDisplayKey] == false
2004                            ) {
2005                                $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '')
2006                                    . self::calcAge(
2007                                        (int)abs($GLOBALS['EXEC_TIME'] - $value),
2008                                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2009                                    )
2010                                    . ')';
2011                            }
2012
2013                            $l = self::date($value) . $ageSuffix;
2014                        }
2015                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'time')) {
2016                        // Handle native time field
2017                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2018                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value . ' UTC');
2019                        } else {
2020                            $value = (int)$value;
2021                        }
2022                        if (!empty($value)) {
2023                            $l = gmdate('H:i', (int)$value);
2024                        }
2025                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'timesec')) {
2026                        // Handle native time field
2027                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2028                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value . ' UTC');
2029                        } else {
2030                            $value = (int)$value;
2031                        }
2032                        if (!empty($value)) {
2033                            $l = gmdate('H:i:s', (int)$value);
2034                        }
2035                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'datetime')) {
2036                        // Handle native datetime field
2037                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') {
2038                            $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value);
2039                        } else {
2040                            $value = (int)$value;
2041                        }
2042                        if (!empty($value)) {
2043                            $l = self::datetime($value);
2044                        }
2045                    } else {
2046                        $l = $value;
2047                    }
2048                }
2049                break;
2050            case 'flex':
2051                $l = strip_tags($value);
2052                break;
2053            default:
2054                if ($defaultPassthrough) {
2055                    $l = $value;
2056                } elseif (isset($theColConf['MM'])) {
2057                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
2058                } elseif ($value) {
2059                    $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200);
2060                }
2061        }
2062        // If this field is a password field, then hide the password by changing it to a random number of asterisk (*)
2063        if (!empty($theColConf['eval']) && stripos($theColConf['eval'], 'password') !== false) {
2064            $l = '';
2065            $randomNumber = random_int(5, 12);
2066            for ($i = 0; $i < $randomNumber; $i++) {
2067                $l .= '*';
2068            }
2069        }
2070        /*****************
2071         *HOOK: post-processing the human readable output from a record
2072         ****************/
2073        $null = null;
2074        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) {
2075            $params = [
2076                'value' => $l,
2077                'colConf' => $theColConf
2078            ];
2079            $l = GeneralUtility::callUserFunction($_funcRef, $params, $null);
2080        }
2081        if ($fixed_lgd_chars) {
2082            return GeneralUtility::fixed_lgd_cs($l, $fixed_lgd_chars);
2083        }
2084        return $l;
2085    }
2086
2087    /**
2088     * 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.
2089     *
2090     * @param string $table Table name, present in TCA
2091     * @param string $fN Field name
2092     * @param string $fV Field value
2093     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
2094     * @param int $uid Uid of the current record
2095     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
2096     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
2097     * @return string
2098     * @see getProcessedValue()
2099     */
2100    public static function getProcessedValueExtra(
2101        $table,
2102        $fN,
2103        $fV,
2104        $fixed_lgd_chars = 0,
2105        $uid = 0,
2106        $forceResult = true,
2107        $pid = 0
2108    ) {
2109        $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, true, false, $uid, $forceResult, $pid);
2110        if (!isset($fVnew)) {
2111            if (is_array($GLOBALS['TCA'][$table])) {
2112                if ($fN == $GLOBALS['TCA'][$table]['ctrl']['tstamp'] || $fN == $GLOBALS['TCA'][$table]['ctrl']['crdate']) {
2113                    $fVnew = self::datetime((int)$fV);
2114                } elseif ($fN === 'pid') {
2115                    // Fetches the path with no regard to the users permissions to select pages.
2116                    $fVnew = self::getRecordPath((int)$fV, '1=1', 20);
2117                } else {
2118                    $fVnew = $fV;
2119                }
2120            }
2121        }
2122        return $fVnew;
2123    }
2124
2125    /**
2126     * Returns fields for a table, $table, which would typically be interesting to select
2127     * This includes uid, the fields defined for title, icon-field.
2128     * 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)
2129     *
2130     * @param string $table Table name, present in $GLOBALS['TCA']
2131     * @param string $prefix Table prefix
2132     * @param array $fields Preset fields (must include prefix if that is used)
2133     * @return string List of fields.
2134     * @internal should only be used from within TYPO3 Core
2135     */
2136    public static function getCommonSelectFields($table, $prefix = '', $fields = [])
2137    {
2138        $fields[] = $prefix . 'uid';
2139        if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
2140            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
2141        }
2142        if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) {
2143            $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
2144            foreach ($secondFields as $fieldN) {
2145                $fields[] = $prefix . $fieldN;
2146            }
2147        }
2148        if (static::isTableWorkspaceEnabled($table)) {
2149            $fields[] = $prefix . 't3ver_state';
2150            $fields[] = $prefix . 't3ver_wsid';
2151            $fields[] = $prefix . 't3ver_count';
2152        }
2153        if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) {
2154            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
2155        }
2156        if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
2157            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
2158        }
2159        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
2160            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2161        }
2162        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) {
2163            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
2164        }
2165        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) {
2166            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
2167        }
2168        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
2169            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
2170        }
2171        return implode(',', array_unique($fields));
2172    }
2173
2174    /*******************************************
2175     *
2176     * Backend Modules API functions
2177     *
2178     *******************************************/
2179
2180    /**
2181     * Returns CSH help text (description), if configured for, as an array (title, description)
2182     *
2183     * @param string $table Table name
2184     * @param string $field Field name
2185     * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
2186     * @internal should only be used from within TYPO3 Core
2187     */
2188    public static function helpTextArray($table, $field)
2189    {
2190        if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
2191            static::getLanguageService()->loadSingleTableDescription($table);
2192        }
2193        $output = [
2194            'description' => null,
2195            'title' => null,
2196            'moreInfo' => false
2197        ];
2198        if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
2199            $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
2200            // Add alternative title, if defined
2201            if ($data['alttitle']) {
2202                $output['title'] = $data['alttitle'];
2203            }
2204            // If we have more information to show and access to the cshmanual
2205            if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax'])
2206                && static::getBackendUserAuthentication()->check('modules', 'help_cshmanual')
2207            ) {
2208                $output['moreInfo'] = true;
2209            }
2210            // Add description
2211            if ($data['description']) {
2212                $output['description'] = $data['description'];
2213            }
2214        }
2215        return $output;
2216    }
2217
2218    /**
2219     * Returns CSH help text
2220     *
2221     * @param string $table Table name
2222     * @param string $field Field name
2223     * @return string HTML content for help text
2224     * @see cshItem()
2225     * @internal should only be used from within TYPO3 Core
2226     */
2227    public static function helpText($table, $field)
2228    {
2229        $helpTextArray = self::helpTextArray($table, $field);
2230        $output = '';
2231        $arrow = '';
2232        // Put header before the rest of the text
2233        if ($helpTextArray['title'] !== null) {
2234            $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
2235        }
2236        // Add see also arrow if we have more info
2237        if ($helpTextArray['moreInfo']) {
2238            /** @var IconFactory $iconFactory */
2239            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2240            $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
2241        }
2242        // Wrap description and arrow in p tag
2243        if ($helpTextArray['description'] !== null || $arrow) {
2244            $output .= '<p class="help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>';
2245        }
2246        return $output;
2247    }
2248
2249    /**
2250     * API function that wraps the text / html in help text, so if a user hovers over it
2251     * the help text will show up
2252     *
2253     * @param string $table The table name for which the help should be shown
2254     * @param string $field The field name for which the help should be shown
2255     * @param string $text The text which should be wrapped with the help text
2256     * @param array $overloadHelpText Array with text to overload help text
2257     * @return string the HTML code ready to render
2258     * @internal should only be used from within TYPO3 Core
2259     */
2260    public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
2261    {
2262        // Initialize some variables
2263        $helpText = '';
2264        $abbrClassAdd = '';
2265        $hasHelpTextOverload = !empty($overloadHelpText);
2266        // Get the help text that should be shown on hover
2267        if (!$hasHelpTextOverload) {
2268            $helpText = self::helpText($table, $field);
2269        }
2270        // If there's a help text or some overload information, proceed with preparing an output
2271        if (!empty($helpText) || $hasHelpTextOverload) {
2272            // If no text was given, just use the regular help icon
2273            if ($text == '') {
2274                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2275                $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2276                $abbrClassAdd = ' help-teaser-icon';
2277            }
2278            $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2279            $wrappedText = '<span class="help-link" data-table="' . $table . '" data-field="' . $field . '"';
2280            // The overload array may provide a title and a description
2281            // If either one is defined, add them to the "data" attributes
2282            if ($hasHelpTextOverload) {
2283                if (isset($overloadHelpText['title'])) {
2284                    $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2285                }
2286                if (isset($overloadHelpText['description'])) {
2287                    $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2288                }
2289            }
2290            $wrappedText .= '>' . $text . '</span>';
2291            return $wrappedText;
2292        }
2293        return $text;
2294    }
2295
2296    /**
2297     * API for getting CSH icons/text for use in backend modules.
2298     * TCA_DESCR will be loaded if it isn't already
2299     *
2300     * @param string $table Table name ('_MOD_'+module name)
2301     * @param string $field Field name (CSH locallang main key)
2302     * @param string $_ (unused)
2303     * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2304     * @return string HTML content for help text
2305     */
2306    public static function cshItem($table, $field, $_ = '', $wrap = '')
2307    {
2308        static::getLanguageService()->loadSingleTableDescription($table);
2309        if (is_array($GLOBALS['TCA_DESCR'][$table])
2310            && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])
2311        ) {
2312            // Creating short description
2313            $output = self::wrapInHelp($table, $field);
2314            if ($output && $wrap) {
2315                $wrParts = explode('|', $wrap);
2316                $output = $wrParts[0] . $output . $wrParts[1];
2317            }
2318            return $output;
2319        }
2320        return '';
2321    }
2322
2323    /**
2324     * Returns a JavaScript string (for an onClick handler) which will load the EditDocumentController script that shows the form for editing of the record(s) you have send as params.
2325     * REMEMBER to always htmlspecialchar() content in href-properties to ampersands get converted to entities (XHTML requirement and XSS precaution)
2326     *
2327     * @param string $params Parameters sent along to EditDocumentController. This requires a much more details description which you must seek in Inside TYPO3s documentation of the FormEngine API. And example could be '&edit[pages][123] = edit' which will show edit form for page record 123.
2328     * @param string $_ (unused)
2329     * @param string $requestUri An optional returnUrl you can set - automatically set to REQUEST_URI.
2330     *
2331     * @return string
2332     * @deprecated will be removed in TYPO3 v11.
2333     */
2334    public static function editOnClick($params, $_ = '', $requestUri = '')
2335    {
2336        trigger_error(__METHOD__ . ' has been marked as deprecated and will be removed in TYPO3 v11. Consider using regular links and use the UriBuilder API instead.', E_USER_DEPRECATED);
2337        if ($requestUri == -1) {
2338            $returnUrl = 'T3_THIS_LOCATION';
2339        } else {
2340            $returnUrl = GeneralUtility::quoteJSvalue(rawurlencode($requestUri ?: GeneralUtility::getIndpEnv('REQUEST_URI')));
2341        }
2342        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2343        return 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('record_edit') . $params . '&returnUrl=') . '+' . $returnUrl . '; return false;';
2344    }
2345
2346    /**
2347     * Returns a JavaScript string for viewing the page id, $id
2348     * It will re-use any window already open.
2349     *
2350     * @param int $pageUid Page UID
2351     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2352     * @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)
2353     * @param string $anchorSection Optional anchor to the URL
2354     * @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!
2355     * @param string $additionalGetVars Additional GET variables.
2356     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2357     * @return string
2358     */
2359    public static function viewOnClick(
2360        $pageUid,
2361        $backPath = '',
2362        $rootLine = null,
2363        $anchorSection = '',
2364        $alternativeUrl = '',
2365        $additionalGetVars = '',
2366        $switchFocus = true
2367    ) {
2368        try {
2369            $previewUrl = self::getPreviewUrl(
2370                $pageUid,
2371                $backPath,
2372                $rootLine,
2373                $anchorSection,
2374                $alternativeUrl,
2375                $additionalGetVars,
2376                $switchFocus
2377            );
2378        } catch (UnableToLinkToPageException $e) {
2379            return '';
2380        }
2381
2382        $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');'
2383            . ($switchFocus ? 'previewWin.focus();' : '') . LF
2384            . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };';
2385
2386        return $onclickCode;
2387    }
2388
2389    /**
2390     * Returns the preview url
2391     *
2392     * It will detect the correct domain name if needed and provide the link with the right back path.
2393     *
2394     * @param int $pageUid Page UID
2395     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2396     * @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)
2397     * @param string $anchorSection Optional anchor to the URL
2398     * @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!
2399     * @param string $additionalGetVars Additional GET variables.
2400     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2401     * @return string
2402     */
2403    public static function getPreviewUrl(
2404        $pageUid,
2405        $backPath = '',
2406        $rootLine = null,
2407        $anchorSection = '',
2408        $alternativeUrl = '',
2409        $additionalGetVars = '',
2410        &$switchFocus = true
2411    ): string {
2412        $viewScript = '/index.php?id=';
2413        if ($alternativeUrl) {
2414            $viewScript = $alternativeUrl;
2415        }
2416
2417        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2418            $hookObj = GeneralUtility::makeInstance($className);
2419            if (method_exists($hookObj, 'preProcess')) {
2420                $hookObj->preProcess(
2421                    $pageUid,
2422                    $backPath,
2423                    $rootLine,
2424                    $anchorSection,
2425                    $viewScript,
2426                    $additionalGetVars,
2427                    $switchFocus
2428                );
2429            }
2430        }
2431
2432        // If there is an alternative URL or the URL has been modified by a hook, use that one.
2433        if ($alternativeUrl || $viewScript !== '/index.php?id=') {
2434            $previewUrl = $viewScript;
2435        } else {
2436            $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2437            $pageInfo = self::readPageAccess($pageUid, $permissionClause) ?: [];
2438            // prepare custom context for link generation (to allow for example time based previews)
2439            $context = clone GeneralUtility::makeInstance(Context::class);
2440            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context);
2441
2442            // Build the URL with a site as prefix, if configured
2443            $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
2444            // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is
2445            $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid);
2446            try {
2447                $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine);
2448            } catch (SiteNotFoundException $e) {
2449                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794919);
2450            }
2451            // Create a multi-dimensional array out of the additional get vars
2452            $additionalQueryParams = [];
2453            parse_str($additionalGetVars, $additionalQueryParams);
2454            if (isset($additionalQueryParams['L'])) {
2455                $additionalQueryParams['_language'] = $additionalQueryParams['_language'] ?? $additionalQueryParams['L'];
2456                unset($additionalQueryParams['L']);
2457            }
2458            try {
2459                $previewUrl = (string)$site->getRouter($context)->generateUri(
2460                    $pageUid,
2461                    $additionalQueryParams,
2462                    $anchorSection,
2463                    RouterInterface::ABSOLUTE_URL
2464                );
2465            } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
2466                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794914);
2467            }
2468        }
2469
2470        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2471            $hookObj = GeneralUtility::makeInstance($className);
2472            if (method_exists($hookObj, 'postProcess')) {
2473                $previewUrl = $hookObj->postProcess(
2474                    $previewUrl,
2475                    $pageUid,
2476                    $rootLine,
2477                    $anchorSection,
2478                    $viewScript,
2479                    $additionalGetVars,
2480                    $switchFocus
2481                );
2482            }
2483        }
2484
2485        return $previewUrl;
2486    }
2487
2488    /**
2489     * Makes click menu link (context sensitive menu)
2490     *
2491     * Returns $str wrapped in a link which will activate the context sensitive
2492     * menu for the record ($table/$uid) or file ($table = file)
2493     * The link will load the top frame with the parameter "&item" which is the table, uid
2494     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2495     *
2496     * @param string $content String to be wrapped in link, typ. image tag.
2497     * @param string $table Table name/File path. If the icon is for a database
2498     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2499     * the absolute filepath
2500     * @param int|string $uid If icon is for database record this is the UID for the
2501     * record from $table or identifier for sys_file record
2502     * @param string $context Set tree if menu is called from tree view
2503     * @param string $_addParams NOT IN USE
2504     * @param string $_enDisItems NOT IN USE
2505     * @param bool $returnTagParameters If set, will return only the onclick
2506     * JavaScript, not the whole link.
2507     *
2508     * @return string The link wrapped input string.
2509     */
2510    public static function wrapClickMenuOnIcon(
2511        $content,
2512        $table,
2513        $uid = 0,
2514        $context = '',
2515        $_addParams = '',
2516        $_enDisItems = '',
2517        $returnTagParameters = false
2518    ) {
2519        $tagParameters = [
2520            'class' => 't3js-contextmenutrigger',
2521            'data-table' => $table,
2522            'data-uid' => (string)$uid,
2523            'data-context' => $context
2524        ];
2525
2526        if ($returnTagParameters) {
2527            return $tagParameters;
2528        }
2529        return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2530    }
2531
2532    /**
2533     * Returns a URL with a command to TYPO3 Datahandler
2534     *
2535     * @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
2536     * @param string|int $redirectUrl Redirect URL, default is to use GeneralUtility::getIndpEnv('REQUEST_URI'), -1 means to generate an URL for JavaScript using T3_THIS_LOCATION
2537     * @return string
2538     */
2539    public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2540    {
2541        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2542        $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2543        if ((int)$redirectUrl === -1) {
2544            trigger_error('Generating URLs to DataHandler for JavaScript click handlers is deprecated. Consider using the href attribute instead.', E_USER_DEPRECATED);
2545            $url = GeneralUtility::quoteJSvalue($url) . '+T3_THIS_LOCATION';
2546        } else {
2547            $url .= rawurlencode((string)($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI')));
2548        }
2549        return $url;
2550    }
2551
2552    /**
2553     * Builds the frontend view domain for a given page ID with a given root
2554     * line.
2555     *
2556     * @param int $pageId The page ID to use, must be > 0
2557     * @param array|null $rootLine The root line structure to use
2558     * @return string The full domain including the protocol http:// or https://, but without the trailing '/'
2559     * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0. Use PageRouter instead.
2560     */
2561    public static function getViewDomain($pageId, $rootLine = null)
2562    {
2563        trigger_error('BackendUtility::getViewDomain() will be removed in TYPO3 v11.0. Use a Site and its PageRouter to link to a page directly', E_USER_DEPRECATED);
2564        $domain = rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), '/');
2565        if (!is_array($rootLine)) {
2566            $rootLine = self::BEgetRootLine($pageId);
2567        }
2568        // Checks alternate domains
2569        if (!empty($rootLine)) {
2570            try {
2571                $site = GeneralUtility::makeInstance(SiteFinder::class)
2572                    ->getSiteByPageId((int)$pageId, $rootLine);
2573                $uri = $site->getBase();
2574            } catch (SiteNotFoundException $e) {
2575                // Just use the current domain
2576                $uri = new Uri($domain);
2577                // Append port number if lockSSLPort is not the standard port 443
2578                $portNumber = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort'];
2579                if ($portNumber > 0 && $portNumber !== 443 && $portNumber < 65536 && $uri->getScheme() === 'https') {
2580                    $uri = $uri->withPort((int)$portNumber);
2581                }
2582            }
2583            return (string)$uri;
2584        }
2585        return $domain;
2586    }
2587
2588    /**
2589     * Returns a selector box "function menu" for a module
2590     * See Inside TYPO3 for details about how to use / make Function menus
2591     *
2592     * @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=...
2593     * @param string $elementName The form elements name, probably something like "SET[...]
2594     * @param string $currentValue The value to be selected currently.
2595     * @param array $menuItems An array with the menu items for the selector box
2596     * @param string $script The script to send the &id to, if empty it's automatically found
2597     * @param string $addParams Additional parameters to pass to the script.
2598     * @return string HTML code for selector box
2599     */
2600    public static function getFuncMenu(
2601        $mainParams,
2602        $elementName,
2603        $currentValue,
2604        $menuItems,
2605        $script = '',
2606        $addParams = ''
2607    ) {
2608        if (!is_array($menuItems) || count($menuItems) <= 1) {
2609            return '';
2610        }
2611        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2612        $options = [];
2613        foreach ($menuItems as $value => $label) {
2614            $options[] = '<option value="'
2615                . htmlspecialchars($value) . '"'
2616                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2617                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2618        }
2619        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2620        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2621        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2622        if (!empty($options)) {
2623            // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2624            $attributes = GeneralUtility::implodeAttributes([
2625                'name' => $elementName,
2626                'class' => 'form-control',
2627                'data-menu-identifier' => $dataMenuIdentifier,
2628                'data-global-event' => 'change',
2629                'data-action-navigate' => '$data=~s/$value/',
2630                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2631            ], true);
2632            return sprintf(
2633                '<select %s>%s</select>',
2634                $attributes,
2635                implode('', $options)
2636            );
2637        }
2638        return '';
2639    }
2640
2641    /**
2642     * Returns a selector box to switch the view
2643     * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose.
2644     * Mingling with getFuncMenu would harm the docHeader Menu.
2645     *
2646     * @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=...
2647     * @param string $elementName The form elements name, probably something like "SET[...]
2648     * @param string $currentValue The value to be selected currently.
2649     * @param array $menuItems An array with the menu items for the selector box
2650     * @param string $script The script to send the &id to, if empty it's automatically found
2651     * @param string $addParams Additional parameters to pass to the script.
2652     * @return string HTML code for selector box
2653     */
2654    public static function getDropdownMenu(
2655        $mainParams,
2656        $elementName,
2657        $currentValue,
2658        $menuItems,
2659        $script = '',
2660        $addParams = ''
2661    ) {
2662        if (!is_array($menuItems) || count($menuItems) <= 1) {
2663            return '';
2664        }
2665        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2666        $options = [];
2667        foreach ($menuItems as $value => $label) {
2668            $options[] = '<option value="'
2669                . htmlspecialchars($value) . '"'
2670                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2671                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2672        }
2673        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2674        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2675        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2676        if (!empty($options)) {
2677            // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2678            $attributes = GeneralUtility::implodeAttributes([
2679                'name' => $elementName,
2680                'data-menu-identifier' => $dataMenuIdentifier,
2681                'data-global-event' => 'change',
2682                'data-action-navigate' => '$data=~s/$value/',
2683                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2684            ], true);
2685            return '
2686			<div class="form-group">
2687				<!-- Function Menu of module -->
2688				<select class="form-control input-sm" ' . $attributes . '>
2689					' . implode(LF, $options) . '
2690				</select>
2691			</div>
2692						';
2693        }
2694        return '';
2695    }
2696
2697    /**
2698     * Checkbox function menu.
2699     * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox.
2700     *
2701     * @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=...
2702     * @param string $elementName The form elements name, probably something like "SET[...]
2703     * @param string $currentValue The value to be selected currently.
2704     * @param string $script The script to send the &id to, if empty it's automatically found
2705     * @param string $addParams Additional parameters to pass to the script.
2706     * @param string $tagParams Additional attributes for the checkbox input tag
2707     * @return string HTML code for checkbox
2708     * @see getFuncMenu()
2709     */
2710    public static function getFuncCheck(
2711        $mainParams,
2712        $elementName,
2713        $currentValue,
2714        $script = '',
2715        $addParams = '',
2716        $tagParams = ''
2717    ) {
2718        // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2719        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2720        $attributes = GeneralUtility::implodeAttributes([
2721            'type' => 'checkbox',
2722            'class' => 'checkbox',
2723            'name' => $elementName,
2724            'value' => '1',
2725            'data-global-event' => 'change',
2726            'data-action-navigate' => '$data=~s/$value/',
2727            'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName),
2728            'data-empty-value' => '0',
2729        ], true);
2730        return
2731            '<input ' . $attributes .
2732            ($currentValue ? ' checked="checked"' : '') .
2733            ($tagParams ? ' ' . $tagParams : '') .
2734            ' />';
2735    }
2736
2737    /**
2738     * Input field function menu
2739     * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange"
2740     *
2741     * @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=...
2742     * @param string $elementName The form elements name, probably something like "SET[...]
2743     * @param string $currentValue The value to be selected currently.
2744     * @param int $size Relative size of input field, max is 48
2745     * @param string $script The script to send the &id to, if empty it's automatically found
2746     * @param string $addParams Additional parameters to pass to the script.
2747     * @return string HTML code for input text field.
2748     * @see getFuncMenu()
2749     */
2750    public static function getFuncInput(
2751        $mainParams,
2752        $elementName,
2753        $currentValue,
2754        $size = 10,
2755        $script = '',
2756        $addParams = ''
2757    ) {
2758        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2759        $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value);';
2760        return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />';
2761    }
2762
2763    /**
2764     * Builds the URL to the current script with given arguments
2765     *
2766     * @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=...
2767     * @param string $addParams Additional parameters to pass to the script.
2768     * @param string $script The script to send the &id to, if empty it's automatically found
2769     * @return string The complete script URL
2770     */
2771    protected static function buildScriptUrl($mainParams, $addParams, $script = '')
2772    {
2773        if (!is_array($mainParams)) {
2774            $mainParams = ['id' => $mainParams];
2775        }
2776        if (!$script) {
2777            $script = PathUtility::basename(Environment::getCurrentScript());
2778        }
2779
2780        if ($routePath = GeneralUtility::_GP('route')) {
2781            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2782            $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
2783            $scriptUrl .= $addParams;
2784        } else {
2785            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
2786        }
2787
2788        return $scriptUrl;
2789    }
2790
2791    /**
2792     * Call to update the page tree frame (or something else..?) after
2793     * use 'updatePageTree' as a first parameter will set the page tree to be updated.
2794     *
2795     * @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.
2796     * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
2797     * @see BackendUtility::getUpdateSignalCode()
2798     */
2799    public static function setUpdateSignal($set = '', $params = '')
2800    {
2801        $beUser = static::getBackendUserAuthentication();
2802        $modData = $beUser->getModuleData(
2803            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
2804            'ses'
2805        );
2806        if ($set) {
2807            $modData[$set] = [
2808                'set' => $set,
2809                'parameter' => $params
2810            ];
2811        } else {
2812            // clear the module data
2813            $modData = [];
2814        }
2815        $beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData);
2816    }
2817
2818    /**
2819     * Call to update the page tree frame (or something else..?) if this is set by the function
2820     * setUpdateSignal(). It will return some JavaScript that does the update
2821     *
2822     * @return string HTML javascript code
2823     * @see BackendUtility::setUpdateSignal()
2824     */
2825    public static function getUpdateSignalCode()
2826    {
2827        $signals = [];
2828        $modData = static::getBackendUserAuthentication()->getModuleData(
2829            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
2830            'ses'
2831        );
2832        if (empty($modData)) {
2833            return '';
2834        }
2835        // Hook: Allows to let TYPO3 execute your JS code
2836        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
2837        // Loop through all setUpdateSignals and get the JS code
2838        foreach ($modData as $set => $val) {
2839            if (isset($updateSignals[$set])) {
2840                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => ''];
2841                $ref = null;
2842                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
2843                $signals[] = $params['JScode'];
2844            } else {
2845                switch ($set) {
2846                    case 'updatePageTree':
2847                        $signals[] = '
2848								if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) {
2849									top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
2850								}
2851							';
2852                        break;
2853                    case 'updateFolderTree':
2854                        $signals[] = '
2855								if (top && top.nav_frame && top.nav_frame.location) {
2856									top.nav_frame.location.reload(true);
2857								}';
2858                        break;
2859                    case 'updateModuleMenu':
2860                        $signals[] = '
2861								if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) {
2862									top.TYPO3.ModuleMenu.App.refreshMenu();
2863								}';
2864                        break;
2865                    case 'updateTopbar':
2866                        $signals[] = '
2867								if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) {
2868									top.TYPO3.Backend.Topbar.refresh();
2869								}';
2870                        break;
2871                }
2872            }
2873        }
2874        $content = implode(LF, $signals);
2875        // For backwards compatibility, should be replaced
2876        self::setUpdateSignal();
2877        return $content;
2878    }
2879
2880    /**
2881     * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
2882     * This is kind of session variable management framework for the backend users.
2883     * 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
2884     * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules.
2885     *
2886     * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus.
2887     * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus.
2888     * @param string $modName modName is the name of this module. Used to get the correct module data.
2889     * @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.
2890     * @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.
2891     * @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)
2892     * @throws \RuntimeException
2893     * @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
2894     */
2895    public static function getModuleData(
2896        $MOD_MENU,
2897        $CHANGED_SETTINGS,
2898        $modName,
2899        $type = '',
2900        $dontValidateList = '',
2901        $setDefaultList = ''
2902    ) {
2903        if ($modName && is_string($modName)) {
2904            // Getting stored user-data from this module:
2905            $beUser = static::getBackendUserAuthentication();
2906            $settings = $beUser->getModuleData($modName, $type);
2907            $changed = 0;
2908            if (!is_array($settings)) {
2909                $changed = 1;
2910                $settings = [];
2911            }
2912            if (is_array($MOD_MENU)) {
2913                foreach ($MOD_MENU as $key => $var) {
2914                    // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array.
2915                    if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) {
2916                        if (is_array($CHANGED_SETTINGS[$key])) {
2917                            $serializedSettings = serialize($CHANGED_SETTINGS[$key]);
2918                            if ((string)$settings[$key] !== $serializedSettings) {
2919                                $settings[$key] = $serializedSettings;
2920                                $changed = 1;
2921                            }
2922                        } else {
2923                            if ((string)$settings[$key] !== (string)$CHANGED_SETTINGS[$key]) {
2924                                $settings[$key] = $CHANGED_SETTINGS[$key];
2925                                $changed = 1;
2926                            }
2927                        }
2928                    }
2929                    // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted
2930                    if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) {
2931                        // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted.
2932                        if (is_array($settings[$key]) || !isset($MOD_MENU[$key][$settings[$key]])) {
2933                            $settings[$key] = (string)key($var);
2934                            $changed = 1;
2935                        }
2936                    }
2937                    // Sets default values (only strings/checkboxes, not menus)
2938                    if ($setDefaultList && !is_array($var)) {
2939                        if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) {
2940                            $settings[$key] = (string)$var;
2941                        }
2942                    }
2943                }
2944            } else {
2945                throw new \RuntimeException('No menu', 1568119229);
2946            }
2947            if ($changed) {
2948                $beUser->pushModuleData($modName, $settings);
2949            }
2950            return $settings;
2951        }
2952        throw new \RuntimeException('Wrong module name "' . $modName . '"', 1568119221);
2953    }
2954
2955    /*******************************************
2956     *
2957     * Core
2958     *
2959     *******************************************/
2960    /**
2961     * Unlock or Lock a record from $table with $uid
2962     * If $table and $uid is not set, then all locking for the current BE_USER is removed!
2963     *
2964     * @param string $table Table name
2965     * @param int $uid Record uid
2966     * @param int $pid Record pid
2967     * @internal
2968     */
2969    public static function lockRecords($table = '', $uid = 0, $pid = 0)
2970    {
2971        $beUser = static::getBackendUserAuthentication();
2972        if (isset($beUser->user['uid'])) {
2973            $userId = (int)$beUser->user['uid'];
2974            if ($table && $uid) {
2975                $fieldsValues = [
2976                    'userid' => $userId,
2977                    'feuserid' => 0,
2978                    'tstamp' => $GLOBALS['EXEC_TIME'],
2979                    'record_table' => $table,
2980                    'record_uid' => $uid,
2981                    'username' => $beUser->user['username'],
2982                    'record_pid' => $pid
2983                ];
2984                GeneralUtility::makeInstance(ConnectionPool::class)
2985                    ->getConnectionForTable('sys_lockedrecords')
2986                    ->insert(
2987                        'sys_lockedrecords',
2988                        $fieldsValues
2989                    );
2990            } else {
2991                GeneralUtility::makeInstance(ConnectionPool::class)
2992                    ->getConnectionForTable('sys_lockedrecords')
2993                    ->delete(
2994                        'sys_lockedrecords',
2995                        ['userid' => (int)$userId]
2996                    );
2997            }
2998        }
2999    }
3000
3001    /**
3002     * Returns information about whether the record from table, $table, with uid, $uid is currently locked
3003     * (edited by another user - which should issue a warning).
3004     * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts
3005     * are activated - which means that a user CAN have a record "open" without having it locked.
3006     * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient.
3007     *
3008     * @param string $table Table name
3009     * @param int $uid Record uid
3010     * @return array|bool
3011     * @internal
3012     */
3013    public static function isRecordLocked($table, $uid)
3014    {
3015        $runtimeCache = self::getRuntimeCache();
3016        $cacheId = 'backend-recordLocked';
3017        $recordLockedCache = $runtimeCache->get($cacheId);
3018        if ($recordLockedCache !== false) {
3019            $lockedRecords = $recordLockedCache;
3020        } else {
3021            $lockedRecords = [];
3022
3023            $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords');
3024            $result = $queryBuilder
3025                ->select('*')
3026                ->from('sys_lockedrecords')
3027                ->where(
3028                    $queryBuilder->expr()->neq(
3029                        'sys_lockedrecords.userid',
3030                        $queryBuilder->createNamedParameter(
3031                            static::getBackendUserAuthentication()->user['uid'],
3032                            \PDO::PARAM_INT
3033                        )
3034                    ),
3035                    $queryBuilder->expr()->gt(
3036                        'sys_lockedrecords.tstamp',
3037                        $queryBuilder->createNamedParameter(
3038                            $GLOBALS['EXEC_TIME'] - 2 * 3600,
3039                            \PDO::PARAM_INT
3040                        )
3041                    )
3042                )
3043                ->execute();
3044
3045            $lang = static::getLanguageService();
3046            while ($row = $result->fetch()) {
3047                // Get the type of the user that locked this record:
3048                if ($row['userid']) {
3049                    $userTypeLabel = 'beUser';
3050                } elseif ($row['feuserid']) {
3051                    $userTypeLabel = 'feUser';
3052                } else {
3053                    $userTypeLabel = 'user';
3054                }
3055                $userType = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel);
3056                // Get the username (if available):
3057                if ($row['username']) {
3058                    $userName = $row['username'];
3059                } else {
3060                    $userName = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.unknownUser');
3061                }
3062                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row;
3063                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf(
3064                    $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'),
3065                    $userType,
3066                    $userName,
3067                    self::calcAge(
3068                        $GLOBALS['EXEC_TIME'] - $row['tstamp'],
3069                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
3070                    )
3071                );
3072                if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) {
3073                    $lockedRecords['pages:' . $row['record_pid']]['msg'] = sprintf(
3074                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'),
3075                        $userType,
3076                        $userName,
3077                        self::calcAge(
3078                            $GLOBALS['EXEC_TIME'] - $row['tstamp'],
3079                            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
3080                        )
3081                    );
3082                }
3083            }
3084            $runtimeCache->set($cacheId, $lockedRecords);
3085        }
3086
3087        return $lockedRecords[$table . ':' . $uid] ?? false;
3088    }
3089
3090    /**
3091     * Returns TSConfig for the TCEFORM object in Page TSconfig.
3092     * Used in TCEFORMs
3093     *
3094     * @param string $table Table name present in TCA
3095     * @param array $row Row from table
3096     * @return array
3097     */
3098    public static function getTCEFORM_TSconfig($table, $row)
3099    {
3100        self::fixVersioningPid($table, $row);
3101        $res = [];
3102        // Get main config for the table
3103        [$TScID, $cPid] = self::getTSCpid($table, $row['uid'], $row['pid']);
3104        if ($TScID >= 0) {
3105            $tsConfig = static::getPagesTSconfig($TScID)['TCEFORM.'][$table . '.'] ?? [];
3106            $typeVal = self::getTCAtypeValue($table, $row);
3107            foreach ($tsConfig as $key => $val) {
3108                if (is_array($val)) {
3109                    $fieldN = substr($key, 0, -1);
3110                    $res[$fieldN] = $val;
3111                    unset($res[$fieldN]['types.']);
3112                    if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'])) {
3113                        ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']);
3114                    }
3115                }
3116            }
3117        }
3118        $res['_CURRENT_PID'] = $cPid;
3119        $res['_THIS_UID'] = $row['uid'];
3120        // So the row will be passed to foreign_table_where_query()
3121        $res['_THIS_ROW'] = $row;
3122        return $res;
3123    }
3124
3125    /**
3126     * Find the real PID of the record (with $uid from $table).
3127     * 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).
3128     * NOTICE: Make sure that the input PID is never negative because the record was an offline version!
3129     * Therefore, you should always use BackendUtility::fixVersioningPid($table,$row); on the data you input before calling this function!
3130     *
3131     * @param string $table Table name
3132     * @param int $uid Record uid
3133     * @param int|string $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return
3134     * @return int
3135     * @internal
3136     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord()
3137     * @see \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid()
3138     */
3139    public static function getTSconfig_pidValue($table, $uid, $pid)
3140    {
3141        // If pid is an integer this takes precedence in our lookup.
3142        if (MathUtility::canBeInterpretedAsInteger($pid)) {
3143            $thePidValue = (int)$pid;
3144            // If ref to another record, look that record up.
3145            if ($thePidValue < 0) {
3146                $pidRec = self::getRecord($table, abs($thePidValue), 'pid');
3147                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
3148            }
3149        } else {
3150            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
3151            $rr = self::getRecord($table, $uid);
3152            $thePidValue = null;
3153            if (is_array($rr)) {
3154                // First check if the t3ver_oid value is greater 0, which means
3155                // it is a workspace element. If so, get the "real" record:
3156                if ((int)($rr['t3ver_oid'] ?? 0) > 0) {
3157                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
3158                    if (is_array($rr)) {
3159                        $thePidValue = $rr['pid'];
3160                    }
3161                } else {
3162                    // Returning the "pid" of the record
3163                    $thePidValue = $rr['pid'];
3164                }
3165            }
3166            if (!$thePidValue) {
3167                // Returns -1 if the record with this pid was not found.
3168                $thePidValue = -1;
3169            }
3170        }
3171        return $thePidValue;
3172    }
3173
3174    /**
3175     * Return the real pid of a record and caches the result.
3176     * The non-cached method needs database queries to do the job, so this method
3177     * can be used if code sometimes calls the same record multiple times to save
3178     * some queries. This should not be done if the calling code may change the
3179     * same record meanwhile.
3180     *
3181     * @param string $table Tablename
3182     * @param string $uid UID value
3183     * @param string $pid PID value
3184     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
3185     */
3186    public static function getTSCpidCached($table, $uid, $pid)
3187    {
3188        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3189        $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: [];
3190        $key = $table . ':' . $uid . ':' . $pid;
3191        if (!isset($firstLevelCache[$key])) {
3192            $firstLevelCache[$key] = static::getTSCpid($table, (int)$uid, (int)$pid);
3193            $runtimeCache->set('backendUtilityTscPidCached', $firstLevelCache);
3194        }
3195        return $firstLevelCache[$key];
3196    }
3197
3198    /**
3199     * 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.
3200     *
3201     * @param string $table Table name
3202     * @param int $uid Record uid
3203     * @param int|string $pid Record pid
3204     * @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,
3205     * second value is the PID value for TSconfig (uid if table is pages, otherwise the pid)
3206     * @internal
3207     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::setHistory()
3208     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::process_datamap()
3209     */
3210    public static function getTSCpid($table, $uid, $pid)
3211    {
3212        // If pid is negative (referring to another record) the pid of the other record is fetched and returned.
3213        $cPid = self::getTSconfig_pidValue($table, $uid, $pid);
3214        // $TScID is the id of $table = pages, else it's the pid of the record.
3215        $TScID = $table === 'pages' && MathUtility::canBeInterpretedAsInteger($uid) ? $uid : $cPid;
3216        return [$TScID, $cPid];
3217    }
3218
3219    /**
3220     * Returns soft-reference parser for the softRef processing type
3221     * Usage: $softRefObj = BackendUtility::softRefParserObj('[parser key]');
3222     *
3223     * @param string $spKey softRef parser key
3224     * @return mixed If available, returns Soft link parser object, otherwise false.
3225     * @internal should only be used from within TYPO3 Core
3226     */
3227    public static function softRefParserObj($spKey)
3228    {
3229        $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser'][$spKey] ?? false;
3230        if ($className) {
3231            return GeneralUtility::makeInstance($className);
3232        }
3233        return false;
3234    }
3235
3236    /**
3237     * Gets an instance of the runtime cache.
3238     *
3239     * @return FrontendInterface
3240     */
3241    protected static function getRuntimeCache()
3242    {
3243        return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3244    }
3245
3246    /**
3247     * Returns array of soft parser references
3248     *
3249     * @param string $parserList softRef parser list
3250     * @return array|bool Array where the parser key is the key and the value is the parameter string, FALSE if no parsers were found
3251     * @throws \InvalidArgumentException
3252     * @internal should only be used from within TYPO3 Core
3253     */
3254    public static function explodeSoftRefParserList($parserList)
3255    {
3256        // Return immediately if list is blank:
3257        if ((string)$parserList === '') {
3258            return false;
3259        }
3260
3261        $runtimeCache = self::getRuntimeCache();
3262        $cacheId = 'backend-softRefList-' . md5($parserList);
3263        $parserListCache = $runtimeCache->get($cacheId);
3264        if ($parserListCache !== false) {
3265            return $parserListCache;
3266        }
3267
3268        // Otherwise parse the list:
3269        $keyList = GeneralUtility::trimExplode(',', $parserList, true);
3270        $output = [];
3271        foreach ($keyList as $val) {
3272            $reg = [];
3273            if (preg_match('/^([[:alnum:]_-]+)\\[(.*)\\]$/', $val, $reg)) {
3274                $output[$reg[1]] = GeneralUtility::trimExplode(';', $reg[2], true);
3275            } else {
3276                $output[$val] = '';
3277            }
3278        }
3279        $runtimeCache->set($cacheId, $output);
3280        return $output;
3281    }
3282
3283    /**
3284     * Returns TRUE if $modName is set and is found as a main- or submodule in $TBE_MODULES array
3285     *
3286     * @param string $modName Module name
3287     * @return bool
3288     */
3289    public static function isModuleSetInTBE_MODULES($modName)
3290    {
3291        $loaded = [];
3292        foreach ($GLOBALS['TBE_MODULES'] as $mkey => $list) {
3293            $loaded[$mkey] = 1;
3294            if (!is_array($list) && trim($list)) {
3295                $subList = GeneralUtility::trimExplode(',', $list, true);
3296                foreach ($subList as $skey) {
3297                    $loaded[$mkey . '_' . $skey] = 1;
3298                }
3299            }
3300        }
3301        return $modName && isset($loaded[$modName]);
3302    }
3303
3304    /**
3305     * Counting references to a record/file
3306     *
3307     * @param string $table Table name (or "_FILE" if its a file)
3308     * @param string $ref Reference: If table, then int-uid, if _FILE, then file reference (relative to Environment::getPublicPath())
3309     * @param string $msg Message with %s, eg. "There were %s records pointing to this file!
3310     * @param string|int|null $count Reference count
3311     * @return string|int Output string (or int count value if no msg string specified)
3312     */
3313    public static function referenceCount($table, $ref, $msg = '', $count = null)
3314    {
3315        if ($count === null) {
3316
3317            // Build base query
3318            $queryBuilder = static::getQueryBuilderForTable('sys_refindex');
3319            $queryBuilder
3320                ->count('*')
3321                ->from('sys_refindex')
3322                ->where(
3323                    $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
3324                    $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
3325                );
3326
3327            // Look up the path:
3328            if ($table === '_FILE') {
3329                if (!GeneralUtility::isFirstPartOfStr($ref, Environment::getPublicPath())) {
3330                    return '';
3331                }
3332
3333                $ref = PathUtility::stripPathSitePrefix($ref);
3334                $queryBuilder->andWhere(
3335                    $queryBuilder->expr()->eq('ref_string', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_STR))
3336                );
3337            } else {
3338                $queryBuilder->andWhere(
3339                    $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT))
3340                );
3341                if ($table === 'sys_file') {
3342                    $queryBuilder->andWhere($queryBuilder->expr()->neq('tablename', $queryBuilder->quote('sys_file_metadata')));
3343                }
3344            }
3345
3346            $count = $queryBuilder->execute()->fetchColumn(0);
3347        }
3348
3349        if ($count) {
3350            return $msg ? sprintf($msg, $count) : $count;
3351        }
3352        return $msg ? '' : 0;
3353    }
3354
3355    /**
3356     * Counting translations of records
3357     *
3358     * @param string $table Table name
3359     * @param string $ref Reference: the record's uid
3360     * @param string $msg Message with %s, eg. "This record has %s translation(s) which will be deleted, too!
3361     * @return string Output string (or int count value if no msg string specified)
3362     */
3363    public static function translationCount($table, $ref, $msg = '')
3364    {
3365        $count = null;
3366        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']
3367            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
3368        ) {
3369            $queryBuilder = static::getQueryBuilderForTable($table);
3370            $queryBuilder->getRestrictions()
3371                ->removeAll()
3372                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3373
3374            $count = (int)$queryBuilder
3375                ->count('*')
3376                ->from($table)
3377                ->where(
3378                    $queryBuilder->expr()->eq(
3379                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3380                        $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)
3381                    ),
3382                    $queryBuilder->expr()->neq(
3383                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
3384                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3385                    )
3386                )
3387                ->execute()
3388                ->fetchColumn(0);
3389        }
3390
3391        if ($count && $msg) {
3392            return sprintf($msg, $count);
3393        }
3394
3395        if ($count) {
3396            return $msg ? sprintf($msg, $count) : $count;
3397        }
3398        return $msg ? '' : 0;
3399    }
3400
3401    /*******************************************
3402     *
3403     * Workspaces / Versioning
3404     *
3405     *******************************************/
3406    /**
3407     * Select all versions of a record, ordered by latest created version (uid DESC)
3408     *
3409     * @param string $table Table name to select from
3410     * @param int $uid Record uid for which to find versions.
3411     * @param string $fields Field list to select
3412     * @param int|null $workspace Search in workspace ID and Live WS, if 0 search only in LiveWS, if NULL search in all WS.
3413     * @param bool $includeDeletedRecords If set, deleted-flagged versions are included! (Only for clean-up script!)
3414     * @param array $row The current record
3415     * @return array|null Array of versions of table/uid
3416     * @internal should only be used from within TYPO3 Core
3417     */
3418    public static function selectVersionsOfRecord(
3419        $table,
3420        $uid,
3421        $fields = '*',
3422        $workspace = 0,
3423        $includeDeletedRecords = false,
3424        $row = null
3425    ) {
3426        $realPid = 0;
3427        $outputRows = [];
3428        if (static::isTableWorkspaceEnabled($table)) {
3429            if (is_array($row) && !$includeDeletedRecords) {
3430                $row['_CURRENT_VERSION'] = true;
3431                $realPid = $row['pid'];
3432                $outputRows[] = $row;
3433            } else {
3434                // Select UID version:
3435                $row = self::getRecord($table, $uid, $fields, '', !$includeDeletedRecords);
3436                // Add rows to output array:
3437                if ($row) {
3438                    $row['_CURRENT_VERSION'] = true;
3439                    $realPid = $row['pid'];
3440                    $outputRows[] = $row;
3441                }
3442            }
3443
3444            $queryBuilder = static::getQueryBuilderForTable($table);
3445            $queryBuilder->getRestrictions()->removeAll();
3446
3447            // build fields to select
3448            $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3449
3450            $queryBuilder
3451                ->from($table)
3452                ->where(
3453                    $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
3454                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
3455                )
3456                ->orderBy('uid', 'DESC');
3457
3458            if (!$includeDeletedRecords) {
3459                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3460            }
3461
3462            if ($workspace === 0) {
3463                // Only in Live WS
3464                $queryBuilder->andWhere(
3465                    $queryBuilder->expr()->eq(
3466                        't3ver_wsid',
3467                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3468                    )
3469                );
3470            } elseif ($workspace !== null) {
3471                // In Live WS and Workspace with given ID
3472                $queryBuilder->andWhere(
3473                    $queryBuilder->expr()->in(
3474                        't3ver_wsid',
3475                        $queryBuilder->createNamedParameter([0, (int)$workspace], Connection::PARAM_INT_ARRAY)
3476                    )
3477                );
3478            }
3479
3480            $rows = $queryBuilder->execute()->fetchAll();
3481
3482            // Add rows to output array:
3483            if (is_array($rows)) {
3484                $outputRows = array_merge($outputRows, $rows);
3485            }
3486            // Set real-pid:
3487            foreach ($outputRows as $idx => $oRow) {
3488                $outputRows[$idx]['_REAL_PID'] = $realPid;
3489            }
3490            return $outputRows;
3491        }
3492        return null;
3493    }
3494
3495    /**
3496     * Find page-tree PID for versionized record
3497     * Will look if the "pid" value of the input record is -1 and if the table supports versioning - if so,
3498     * it will translate the -1 PID into the PID of the original record
3499     * Used whenever you are tracking something back, like making the root line.
3500     * Will only translate if the workspace of the input record matches that of the current user (unless flag set)
3501     * Principle; Record offline! => Find online?
3502     *
3503     * If the record had its pid corrected to the online versions pid, then "_ORIG_pid" is set
3504     * to the original pid value (-1 of course). The field "_ORIG_pid" is used by various other functions
3505     * to detect if a record was in fact in a versionized branch.
3506     *
3507     * @param string $table Table name
3508     * @param array $rr Record array passed by reference. As minimum, "pid" and "uid" fields must exist! "t3ver_oid", "t3ver_state" and "t3ver_wsid" is nice and will save you a DB query.
3509     * @param bool $ignoreWorkspaceMatch Ignore workspace match
3510     * @see PageRepository::fixVersioningPid()
3511     * @internal should only be used from within TYPO3 Core
3512     */
3513    public static function fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch = false)
3514    {
3515        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3516            return;
3517        }
3518        if (!static::isTableWorkspaceEnabled($table)) {
3519            return;
3520        }
3521        // Check that the input record is an offline version from a table that supports versioning
3522        if (!is_array($rr)) {
3523            return;
3524        }
3525        $incomingPid = $rr['pid'] ?? null;
3526        // Check values for t3ver_oid and t3ver_wsid:
3527        if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid']) && isset($rr['t3ver_state'])) {
3528            // If "t3ver_oid" is already a field, just set this:
3529            $oid = $rr['t3ver_oid'];
3530            $workspaceId = (int)$rr['t3ver_wsid'];
3531            $versionState = (int)$rr['t3ver_state'];
3532        } else {
3533            $oid = 0;
3534            $workspaceId = 0;
3535            $versionState = 0;
3536            // Otherwise we have to expect "uid" to be in the record and look up based on this:
3537            $newPidRec = self::getRecord($table, $rr['uid'], 'pid,t3ver_oid,t3ver_wsid,t3ver_state');
3538            if (is_array($newPidRec)) {
3539                $incomingPid = $newPidRec['pid'];
3540                $oid = $newPidRec['t3ver_oid'];
3541                $workspaceId = $newPidRec['t3ver_wsid'];
3542                $versionState = $newPidRec['t3ver_state'];
3543            }
3544        }
3545        if ($oid && ($ignoreWorkspaceMatch || (static::getBackendUserAuthentication() instanceof BackendUserAuthentication && $workspaceId === (int)static::getBackendUserAuthentication()->workspace))) {
3546            if ($incomingPid === null) {
3547                // This can be removed, as this is the same for all versioned records
3548                $onlineRecord = self::getRecord($table, $oid, 'pid');
3549                if (is_array($onlineRecord)) {
3550                    $rr['_ORIG_pid'] = $onlineRecord['pid'];
3551                    $rr['pid'] = $onlineRecord['pid'];
3552                }
3553            } else {
3554                // This can be removed, as this is the same for all versioned records (clearly obvious here)
3555                $rr['_ORIG_pid'] = $incomingPid;
3556                $rr['pid'] = $incomingPid;
3557            }
3558            // Use moved PID in case of move pointer
3559            if ($versionState === VersionState::MOVE_POINTER) {
3560                if ($incomingPid !== null) {
3561                    $movedPageIdInWorkspace = $incomingPid;
3562                } else {
3563                    $versionedMovePointer = self::getRecord($table, $rr['uid'], 'pid');
3564                    $movedPageIdInWorkspace = $versionedMovePointer['pid'];
3565                }
3566                $rr['_ORIG_pid'] = $incomingPid;
3567                $rr['pid'] = $movedPageIdInWorkspace;
3568            }
3569        }
3570    }
3571
3572    /**
3573     * Workspace Preview Overlay
3574     * Generally ALWAYS used when records are selected based on uid or pid.
3575     * If records are selected on other fields than uid or pid (eg. "email = ....")
3576     * then usage might produce undesired results and that should be evaluated on individual basis.
3577     * Principle; Record online! => Find offline?
3578     * Recently, this function has been modified so it MAY set $row to FALSE.
3579     * This happens if a version overlay with the move-id pointer is found in which case we would like a backend preview.
3580     * In other words, you should check if the input record is still an array afterwards when using this function.
3581     *
3582     * @param string $table Table name
3583     * @param array $row Record array passed by reference. As minimum, the "uid" and  "pid" fields must exist! Fake fields cannot exist since the fields in the array is used as field names in the SQL look up. It would be nice to have fields like "t3ver_state" and "t3ver_mode_id" as well to avoid a new lookup inside movePlhOL().
3584     * @param int $wsid Workspace ID, if not specified will use static::getBackendUserAuthentication()->workspace
3585     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
3586     * @see fixVersioningPid()
3587     */
3588    public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false)
3589    {
3590        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3591            return;
3592        }
3593        // If this is FALSE the placeholder is shown raw in the backend.
3594        // I don't know if this move can be useful for users to toggle. Technically it can help debugging.
3595        $previewMovePlaceholders = true;
3596        // Initialize workspace ID
3597        if ($wsid == -99 && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3598            $wsid = static::getBackendUserAuthentication()->workspace;
3599        }
3600        // Check if workspace is different from zero and record is set:
3601        if ($wsid !== 0 && is_array($row)) {
3602            // Check if input record is a move-placeholder and if so, find the pointed-to live record:
3603            $movePldSwap = null;
3604            $orig_uid = 0;
3605            $orig_pid = 0;
3606            if ($previewMovePlaceholders) {
3607                $orig_uid = $row['uid'];
3608                $orig_pid = $row['pid'];
3609                $movePldSwap = self::movePlhOL($table, $row);
3610            }
3611            $wsAlt = self::getWorkspaceVersionOfRecord(
3612                $wsid,
3613                $table,
3614                $row['uid'],
3615                implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3616            );
3617            // If version was found, swap the default record with that one.
3618            if (is_array($wsAlt)) {
3619                // Check if this is in move-state:
3620                if ($previewMovePlaceholders && !$movePldSwap && static::isTableWorkspaceEnabled($table) && $unsetMovePointers) {
3621                    // Only for WS ver 2... (moving)
3622                    // If t3ver_state is not found, then find it... (but we like best if it is here...)
3623                    if (!isset($wsAlt['t3ver_state'])) {
3624                        $stateRec = self::getRecord($table, $wsAlt['uid'], 't3ver_state');
3625                        $versionState = VersionState::cast($stateRec['t3ver_state']);
3626                    } else {
3627                        $versionState = VersionState::cast($wsAlt['t3ver_state']);
3628                    }
3629                    if ($versionState->equals(VersionState::MOVE_POINTER)) {
3630                        // @todo Same problem as frontend in versionOL(). See TODO point there.
3631                        $row = false;
3632                        return;
3633                    }
3634                }
3635                // Always correct PID from -1 to what it should be
3636                if (isset($wsAlt['pid'])) {
3637                    // Keep the old (-1) - indicates it was a version.
3638                    $wsAlt['_ORIG_pid'] = $wsAlt['pid'];
3639                    // Set in the online versions PID.
3640                    $wsAlt['pid'] = $row['pid'];
3641                }
3642                // For versions of single elements or page+content, swap UID and PID
3643                $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
3644                $wsAlt['uid'] = $row['uid'];
3645                // Backend css class:
3646                $wsAlt['_CSSCLASS'] = 'ver-element';
3647                // Changing input record to the workspace version alternative:
3648                $row = $wsAlt;
3649            }
3650            // If the original record was a move placeholder, the uid and pid of that is preserved here:
3651            if ($movePldSwap) {
3652                $row['_MOVE_PLH'] = true;
3653                $row['_MOVE_PLH_uid'] = $orig_uid;
3654                $row['_MOVE_PLH_pid'] = $orig_pid;
3655                // For display; To make the icon right for the placeholder vs. the original
3656                $row['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
3657            }
3658        }
3659    }
3660
3661    /**
3662     * Checks if record is a move-placeholder (t3ver_state==VersionState::MOVE_PLACEHOLDER) and if so
3663     * it will set $row to be the pointed-to live record (and return TRUE)
3664     *
3665     * @param string $table Table name
3666     * @param array $row Row (passed by reference) - must be online record!
3667     * @return bool TRUE if overlay is made.
3668     * @see PageRepository::movePlhOl()
3669     * @internal should only be used from within TYPO3 Core
3670     */
3671    public static function movePlhOL($table, &$row)
3672    {
3673        if (static::isTableWorkspaceEnabled($table)) {
3674            // If t3ver_move_id or t3ver_state is not found, then find it... (but we like best if it is here...)
3675            if (!isset($row['t3ver_move_id']) || !isset($row['t3ver_state'])) {
3676                $moveIDRec = self::getRecord($table, $row['uid'], 't3ver_move_id, t3ver_state');
3677                $moveID = $moveIDRec['t3ver_move_id'];
3678                $versionState = VersionState::cast($moveIDRec['t3ver_state']);
3679            } else {
3680                $moveID = $row['t3ver_move_id'];
3681                $versionState = VersionState::cast($row['t3ver_state']);
3682            }
3683            // Find pointed-to record.
3684            if ($versionState->equals(VersionState::MOVE_PLACEHOLDER) && $moveID) {
3685                if ($origRow = self::getRecord(
3686                    $table,
3687                    $moveID,
3688                    implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3689                )) {
3690                    $row = $origRow;
3691                    return true;
3692                }
3693            }
3694        }
3695        return false;
3696    }
3697
3698    /**
3699     * Select the workspace version of a record, if exists
3700     *
3701     * @param int $workspace Workspace ID
3702     * @param string $table Table name to select from
3703     * @param int $uid Record uid for which to find workspace version.
3704     * @param string $fields Field list to select
3705     * @return array|bool If found, return record, otherwise false
3706     */
3707    public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*')
3708    {
3709        if (ExtensionManagementUtility::isLoaded('workspaces')) {
3710            if ($workspace !== 0 && self::isTableWorkspaceEnabled($table)) {
3711
3712                // Select workspace version of record:
3713                $queryBuilder = static::getQueryBuilderForTable($table);
3714                $queryBuilder->getRestrictions()
3715                    ->removeAll()
3716                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3717
3718                // build fields to select
3719                $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3720
3721                $row = $queryBuilder
3722                    ->from($table)
3723                    ->where(
3724                        $queryBuilder->expr()->eq(
3725                            't3ver_oid',
3726                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3727                        ),
3728                        $queryBuilder->expr()->eq(
3729                            't3ver_wsid',
3730                            $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
3731                        )
3732                    )
3733                    ->execute()
3734                    ->fetch();
3735
3736                return $row;
3737            }
3738        }
3739        return false;
3740    }
3741
3742    /**
3743     * Returns live version of record
3744     *
3745     * @param string $table Table name
3746     * @param int $uid Record UID of draft, offline version
3747     * @param string $fields Field list, default is *
3748     * @return array|null If found, the record, otherwise NULL
3749     */
3750    public static function getLiveVersionOfRecord($table, $uid, $fields = '*')
3751    {
3752        $liveVersionId = self::getLiveVersionIdOfRecord($table, $uid);
3753        if ($liveVersionId !== null) {
3754            return self::getRecord($table, $liveVersionId, $fields);
3755        }
3756        return null;
3757    }
3758
3759    /**
3760     * Gets the id of the live version of a record.
3761     *
3762     * @param string $table Name of the table
3763     * @param int $uid Uid of the offline/draft record
3764     * @return int|null The id of the live version of the record (or NULL if nothing was found)
3765     * @internal should only be used from within TYPO3 Core
3766     */
3767    public static function getLiveVersionIdOfRecord($table, $uid)
3768    {
3769        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3770            return null;
3771        }
3772        $liveVersionId = null;
3773        if (self::isTableWorkspaceEnabled($table)) {
3774            $currentRecord = self::getRecord($table, $uid, 'pid,t3ver_oid');
3775            if (is_array($currentRecord) && (int)$currentRecord['t3ver_oid'] > 0) {
3776                $liveVersionId = $currentRecord['t3ver_oid'];
3777            }
3778        }
3779        return $liveVersionId;
3780    }
3781
3782    /**
3783     * Will return where clause de-selecting new(/deleted)-versions from other workspaces.
3784     * If in live-workspace, don't show "MOVE-TO-PLACEHOLDERS" records if versioningWS is 2 (allows moving)
3785     *
3786     * @param string $table Table name
3787     * @return string Where clause if applicable.
3788     * @internal should only be used from within TYPO3 Core
3789     */
3790    public static function versioningPlaceholderClause($table)
3791    {
3792        if (static::isTableWorkspaceEnabled($table) && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3793            $currentWorkspace = (int)static::getBackendUserAuthentication()->workspace;
3794            return ' AND (' . $table . '.t3ver_state <= ' . new VersionState(VersionState::DEFAULT_STATE) . ' OR ' . $table . '.t3ver_wsid = ' . $currentWorkspace . ')';
3795        }
3796        return '';
3797    }
3798
3799    /**
3800     * Get additional where clause to select records of a specific workspace (includes live as well).
3801     *
3802     * @param string $table Table name
3803     * @param int $workspaceId Workspace ID
3804     * @return string Workspace where clause
3805     * @internal should only be used from within TYPO3 Core
3806     */
3807    public static function getWorkspaceWhereClause($table, $workspaceId = null)
3808    {
3809        $whereClause = '';
3810        if (self::isTableWorkspaceEnabled($table) && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3811            if ($workspaceId === null) {
3812                $workspaceId = static::getBackendUserAuthentication()->workspace;
3813            }
3814            $workspaceId = (int)$workspaceId;
3815            $comparison = $workspaceId === 0 ? '=' : '>';
3816            $whereClause = ' AND ' . $table . '.t3ver_wsid=' . $workspaceId . ' AND ' . $table . '.t3ver_oid' . $comparison . '0';
3817        }
3818        return $whereClause;
3819    }
3820
3821    /**
3822     * Performs mapping of new uids to new versions UID in case of import inside a workspace.
3823     *
3824     * @param string $table Table name
3825     * @param int $uid Record uid (of live record placeholder)
3826     * @return int Uid of offline version if any, otherwise live uid.
3827     * @internal should only be used from within TYPO3 Core
3828     */
3829    public static function wsMapId($table, $uid)
3830    {
3831        $wsRec = null;
3832        if (static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3833            $wsRec = self::getWorkspaceVersionOfRecord(
3834                static::getBackendUserAuthentication()->workspace,
3835                $table,
3836                $uid,
3837                'uid'
3838            );
3839        }
3840        return is_array($wsRec) ? $wsRec['uid'] : $uid;
3841    }
3842
3843    /**
3844     * Returns move placeholder of online (live) version
3845     *
3846     * @param string $table Table name
3847     * @param int $uid Record UID of online version
3848     * @param string $fields Field list, default is *
3849     * @param int|null $workspace The workspace to be used
3850     * @return array|bool If found, the record, otherwise false
3851     * @internal should only be used from within TYPO3 Core
3852     */
3853    public static function getMovePlaceholder($table, $uid, $fields = '*', $workspace = null)
3854    {
3855        if ($workspace === null && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3856            $workspace = static::getBackendUserAuthentication()->workspace;
3857        }
3858        if ((int)$workspace !== 0 && static::isTableWorkspaceEnabled($table)) {
3859            // Select workspace version of record:
3860            $queryBuilder = static::getQueryBuilderForTable($table);
3861            $queryBuilder->getRestrictions()
3862                ->removeAll()
3863                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3864
3865            $row = $queryBuilder
3866                ->select(...GeneralUtility::trimExplode(',', $fields, true))
3867                ->from($table)
3868                ->where(
3869                    $queryBuilder->expr()->eq(
3870                        't3ver_state',
3871                        $queryBuilder->createNamedParameter(
3872                            (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
3873                            \PDO::PARAM_INT
3874                        )
3875                    ),
3876                    $queryBuilder->expr()->eq(
3877                        't3ver_move_id',
3878                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3879                    ),
3880                    $queryBuilder->expr()->eq(
3881                        't3ver_wsid',
3882                        $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
3883                    )
3884                )
3885                ->execute()
3886                ->fetch();
3887
3888            return $row ?: false;
3889        }
3890        return false;
3891    }
3892
3893    /*******************************************
3894     *
3895     * Miscellaneous
3896     *
3897     *******************************************/
3898    /**
3899     * Prints TYPO3 Copyright notice for About Modules etc. modules.
3900     *
3901     * Warning:
3902     * DO NOT prevent this notice from being shown in ANY WAY.
3903     * According to the GPL license an interactive application must show such a notice on start-up ('If the program is interactive, make it output a short notice... ' - see GPL.txt)
3904     * Therefore preventing this notice from being properly shown is a violation of the license, regardless of whether you remove it or use a stylesheet to obstruct the display.
3905     *
3906     * @return string Text/Image (HTML) for copyright notice.
3907     * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11.0
3908     */
3909    public static function TYPO3_copyRightNotice()
3910    {
3911        trigger_error('BackendUtility::TYPO3_copyRightNotice() will be removed in TYPO3 v11.0, use the Typo3Information PHP class instead.', E_USER_DEPRECATED);
3912        $copyrightGenerator = GeneralUtility::makeInstance(Typo3Information::class, static::getLanguageService());
3913        return $copyrightGenerator->getCopyrightNotice();
3914    }
3915
3916    /**
3917     * Creates ADMCMD parameters for the "viewpage" extension / frontend
3918     *
3919     * @param array $pageInfo Page record
3920     * @param \TYPO3\CMS\Core\Context\Context $context
3921     * @return string Query-parameters
3922     * @internal
3923     */
3924    public static function ADMCMD_previewCmds($pageInfo, Context $context)
3925    {
3926        if ($pageInfo === []) {
3927            return '';
3928        }
3929        // Initialize access restriction values from current page
3930        $access = [
3931            'fe_group' => (string)($pageInfo['fe_group'] ?? ''),
3932            'starttime' => (int)($pageInfo['starttime'] ?? 0),
3933            'endtime' => (int)($pageInfo['endtime'] ?? 0)
3934        ];
3935        // Only check rootline if the current page has not set extendToSubpages itself
3936        if (!(bool)($pageInfo['extendToSubpages'] ?? false)) {
3937            $rootline = self::BEgetRootLine((int)($pageInfo['uid'] ?? 0));
3938            // remove the current page from the rootline
3939            array_shift($rootline);
3940            foreach ($rootline as $page) {
3941                // Skip root node, invalid pages and pages which do not define extendToSubpages
3942                if ((int)($page['uid'] ?? 0) <= 0 || !(bool)($page['extendToSubpages'] ?? false)) {
3943                    continue;
3944                }
3945                $access['fe_group'] = (string)($page['fe_group'] ?? '');
3946                $access['starttime'] = (int)($page['starttime'] ?? 0);
3947                $access['endtime'] = (int)($page['endtime'] ?? 0);
3948                // Stop as soon as a page in the rootline has extendToSubpages set
3949                break;
3950            }
3951        }
3952        $simUser = '';
3953        $simTime = '';
3954        if ((int)$access['fe_group'] === -2) {
3955            // -2 means "show at any login". We simulate first available fe_group.
3956            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3957                ->getQueryBuilderForTable('fe_groups');
3958            $queryBuilder->getRestrictions()
3959                ->removeAll()
3960                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3961                ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
3962
3963            $activeFeGroupId = $queryBuilder->select('uid')
3964                ->from('fe_groups')
3965                ->execute()
3966                ->fetchColumn();
3967
3968            if ($activeFeGroupId) {
3969                $simUser = '&ADMCMD_simUser=' . $activeFeGroupId;
3970            }
3971        } elseif (!empty($access['fe_group'])) {
3972            $simUser = '&ADMCMD_simUser=' . $access['fe_group'];
3973        }
3974        if ($access['starttime'] > $GLOBALS['EXEC_TIME']) {
3975            // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
3976            // an URL for it
3977            $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $access['starttime']));
3978            $context->setAspect('date', $dateAspect);
3979            $simTime = '&ADMCMD_simTime=' . $access['starttime'];
3980        }
3981        if ($access['endtime'] < $GLOBALS['EXEC_TIME'] && $access['endtime'] !== 0) {
3982            // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
3983            // in turn PageRouter will generate an URL for it
3984            $dateAspect = GeneralUtility::makeInstance(
3985                DateTimeAspect::class,
3986                new \DateTimeImmutable('@' . ($access['endtime'] - 1))
3987            );
3988            $context->setAspect('date', $dateAspect);
3989            $simTime = '&ADMCMD_simTime=' . ($access['endtime'] - 1);
3990        }
3991        return $simUser . $simTime;
3992    }
3993
3994    /**
3995     * Returns the name of the backend script relative to the TYPO3 main directory.
3996     *
3997     * @param string $interface Name of the backend interface  (backend, frontend) to look up the script name for. If no interface is given, the interface for the current backend user is used.
3998     * @return string The name of the backend script relative to the TYPO3 main directory.
3999     * @internal should only be used from within TYPO3 Core
4000     */
4001    public static function getBackendScript($interface = '')
4002    {
4003        if (!$interface) {
4004            $interface = static::getBackendUserAuthentication()->uc['interfaceSetup'];
4005        }
4006        switch ($interface) {
4007            case 'frontend':
4008                $script = '../.';
4009                break;
4010            case 'backend':
4011            default:
4012                $script = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('main');
4013        }
4014        return $script;
4015    }
4016
4017    /**
4018     * Determines whether a table is enabled for workspaces.
4019     *
4020     * @param string $table Name of the table to be checked
4021     * @return bool
4022     */
4023    public static function isTableWorkspaceEnabled($table)
4024    {
4025        return !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']);
4026    }
4027
4028    /**
4029     * Gets the TCA configuration of a field.
4030     *
4031     * @param string $table Name of the table
4032     * @param string $field Name of the field
4033     * @return array
4034     */
4035    public static function getTcaFieldConfiguration($table, $field)
4036    {
4037        $configuration = [];
4038        if (isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4039            $configuration = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4040        }
4041        return $configuration;
4042    }
4043
4044    /**
4045     * Whether to ignore restrictions on a web-mount of a table.
4046     * The regular behaviour is that records to be accessed need to be
4047     * in a valid user's web-mount.
4048     *
4049     * @param string $table Name of the table
4050     * @return bool
4051     */
4052    public static function isWebMountRestrictionIgnored($table)
4053    {
4054        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreWebMountRestriction']);
4055    }
4056
4057    /**
4058     * Whether to ignore restrictions on root-level records.
4059     * The regular behaviour is that records on the root-level (page-id 0)
4060     * only can be accessed by admin users.
4061     *
4062     * @param string $table Name of the table
4063     * @return bool
4064     */
4065    public static function isRootLevelRestrictionIgnored($table)
4066    {
4067        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']);
4068    }
4069
4070    /**
4071     * @param string $table
4072     * @return Connection
4073     */
4074    protected static function getConnectionForTable($table)
4075    {
4076        return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
4077    }
4078
4079    /**
4080     * @param string $table
4081     * @return QueryBuilder
4082     */
4083    protected static function getQueryBuilderForTable($table)
4084    {
4085        return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4086    }
4087
4088    /**
4089     * @return LoggerInterface
4090     */
4091    protected static function getLogger()
4092    {
4093        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
4094    }
4095
4096    /**
4097     * @return LanguageService
4098     */
4099    protected static function getLanguageService()
4100    {
4101        return $GLOBALS['LANG'];
4102    }
4103
4104    /**
4105     * @return BackendUserAuthentication|null
4106     */
4107    protected static function getBackendUserAuthentication()
4108    {
4109        return $GLOBALS['BE_USER'] ?? null;
4110    }
4111}
4112