BackendUtility   F
last analyzed

Complexity

Total Complexity 572

Size/Duplication

Total Lines 3707
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 572
eloc 1699
dl 0
loc 3707
rs 0.8
c 5
b 0
f 0

86 Methods

Rating   Name   Duplication   Size   Complexity  
A getRecordWSOL() 0 24 6
A getRecord() 0 31 6
C getTCAtypeValue() 0 48 12
A purgeComputedPropertiesFromRecord() 0 8 1
A purgeComputedPropertyNames() 0 6 1
A getPagesTSconfig() 0 53 5
B readPageAccess() 0 20 7
B calcAge() 0 19 9
C getRecordTitle() 0 64 16
A date() 0 3 1
A time() 0 3 2
B getRecordPath() 0 24 8
A isTableLocalizable() 0 8 6
A daysUntil() 0 4 1
A getGroupNames() 0 7 1
A dateTimeAge() 0 8 3
A getRecordTitlePrep() 0 13 5
A getNoRecordTitle() 0 9 2
A getRecordsSortedByTitle() 0 31 2
A datetime() 0 5 1
A getUserNames() 0 7 1
A getFuncInput() 0 11 1
A splitTable_Uid() 0 6 1
A getTCEFORM_TSconfig() 0 24 6
C fixVersioningPid() 0 44 14
A isRootLevelRestrictionIgnored() 0 3 1
A getQueryBuilderForTable() 0 3 1
A viewOnClick() 0 32 3
A getRuntimeCache() 0 3 1
A lockRecords() 0 27 4
B referenceCount() 0 39 8
A getProcessedValueExtra() 0 23 6
F getProcessedValue() 0 432 100
B wrapInHelp() 0 34 8
B getDropdownMenu() 0 41 6
A getLogger() 0 3 1
B helpTextArray() 0 30 8
A getLiveVersionOfRecord() 0 7 2
B getLabelFromItemListMerged() 0 26 11
B BEenableFields() 0 51 7
A helpText() 0 20 5
A getLinkToDataHandlerAction() 0 6 2
B getUpdateSignalCode() 0 52 8
A getLanguageService() 0 3 1
A getItemLabel() 0 3 1
B translationCount() 0 32 6
A getTSCpidCached() 0 10 3
C workspaceOL() 0 62 16
F getCommonSelectFields() 0 35 12
C ADMCMD_previewCmds() 0 68 12
A getTSCpid() 0 7 3
A getLabelFromItemlist() 0 13 4
A getRecordLocalization() 0 36 3
A cshItem() 0 15 5
B BEgetRootLine() 0 63 10
A getConnectionForTable() 0 3 1
A isModuleSetInTBE_MODULES() 0 13 6
F titleAttribForPages() 0 80 23
A getLiveVersionIdOfRecord() 0 18 6
F getModuleData() 0 62 21
A wsMapId() 0 12 3
A getTcaFieldConfiguration() 0 7 2
B isRecordLocked() 0 81 8
A getBackendUserAuthentication() 0 3 1
A getFuncCheck() 0 25 3
B getTSconfig_pidValue() 0 33 8
B resolveFileReferences() 0 57 10
A isTableWorkspaceEnabled() 0 3 1
A getRecordToolTip() 0 7 1
C getPreviewUrl() 0 83 12
A buildScriptUrl() 0 20 5
B thumbCode() 0 81 10
A softRefParserObj() 0 7 2
B getPageForRootline() 0 69 10
A isWebMountRestrictionIgnored() 0 3 1
A getThumbnailUrl() 0 9 1
C getLabelsFromItemsList() 0 39 12
A getWorkspaceVersionOfRecord() 0 46 4
B getFuncMenu() 0 39 6
B selectVersionsOfRecord() 0 68 9
C getRecordIconAltText() 0 37 13
A openPageTree() 0 28 5
A setUpdateSignal() 0 17 2
A wrapClickMenuOnIcon() 0 22 4
A explodeSoftRefParserList() 0 27 5
A getClickMenuOnIconTagParameters() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like BackendUtility often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BackendUtility, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Utility;
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use Psr\Log\LoggerInterface;
20
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
21
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
22
use TYPO3\CMS\Backend\Routing\Route;
23
use TYPO3\CMS\Backend\Routing\UriBuilder;
24
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
25
use TYPO3\CMS\Core\Cache\CacheManager;
26
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
27
use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
28
use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
29
use TYPO3\CMS\Core\Context\Context;
30
use TYPO3\CMS\Core\Context\DateTimeAspect;
31
use TYPO3\CMS\Core\Core\Environment;
32
use TYPO3\CMS\Core\Database\Connection;
33
use TYPO3\CMS\Core\Database\ConnectionPool;
34
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
35
use TYPO3\CMS\Core\Database\Query\QueryHelper;
36
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
37
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
38
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
39
use TYPO3\CMS\Core\Database\RelationHandler;
40
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
41
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
42
use TYPO3\CMS\Core\Imaging\Icon;
43
use TYPO3\CMS\Core\Imaging\IconFactory;
44
use TYPO3\CMS\Core\Imaging\ImageDimension;
45
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
46
use TYPO3\CMS\Core\Localization\LanguageService;
47
use TYPO3\CMS\Core\Log\LogManager;
48
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
49
use TYPO3\CMS\Core\Resource\ProcessedFile;
50
use TYPO3\CMS\Core\Resource\ResourceFactory;
51
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
52
use TYPO3\CMS\Core\Routing\RouterInterface;
53
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
54
use TYPO3\CMS\Core\Site\SiteFinder;
55
use TYPO3\CMS\Core\Type\Bitmask\Permission;
56
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
57
use TYPO3\CMS\Core\Utility\ArrayUtility;
58
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
59
use TYPO3\CMS\Core\Utility\GeneralUtility;
60
use TYPO3\CMS\Core\Utility\HttpUtility;
61
use TYPO3\CMS\Core\Utility\MathUtility;
62
use TYPO3\CMS\Core\Utility\PathUtility;
63
use TYPO3\CMS\Core\Utility\StringUtility;
64
use 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
 */
74
class 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|string $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 = StringUtility::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
        $split = explode('_', strrev($str), 2);
211
        $uid = $split[0];
212
        $table = $split[1] ?? '';
213
        return [strrev($table), strrev($uid)];
214
    }
215
216
    /**
217
     * Backend implementation of enableFields()
218
     * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime.
219
     * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition.
220
     * $GLOBALS["SIM_ACCESS_TIME"] is used for date.
221
     *
222
     * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA'].
223
     * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection)
224
     * @return string WHERE clause part
225
     * @internal should only be used from within TYPO3 Core, but DefaultRestrictionHandler is recommended as alternative
226
     */
227
    public static function BEenableFields($table, $inv = false)
228
    {
229
        $ctrl = $GLOBALS['TCA'][$table]['ctrl'] ?? [];
230
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
231
            ->getConnectionForTable($table)
232
            ->getExpressionBuilder();
233
        $query = $expressionBuilder->andX();
234
        $invQuery = $expressionBuilder->orX();
235
236
        $ctrl += [
237
            'enablecolumns' => [],
238
        ];
239
240
        if (is_array($ctrl)) {
241
            if ($ctrl['enablecolumns']['disabled'] ?? false) {
242
                $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
243
                $query->add($expressionBuilder->eq($field, 0));
244
                $invQuery->add($expressionBuilder->neq($field, 0));
245
            }
246
            if ($ctrl['enablecolumns']['starttime'] ?? false) {
247
                $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
248
                $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']));
249
                $invQuery->add(
250
                    $expressionBuilder->andX(
251
                        $expressionBuilder->neq($field, 0),
252
                        $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
253
                    )
254
                );
255
            }
256
            if ($ctrl['enablecolumns']['endtime'] ?? false) {
257
                $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
258
                $query->add(
259
                    $expressionBuilder->orX(
260
                        $expressionBuilder->eq($field, 0),
261
                        $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
262
                    )
263
                );
264
                $invQuery->add(
265
                    $expressionBuilder->andX(
266
                        $expressionBuilder->neq($field, 0),
267
                        $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
268
                    )
269
                );
270
            }
271
        }
272
273
        if ($query->count() === 0) {
274
            return '';
275
        }
276
277
        return ' AND ' . ($inv ? $invQuery : $query);
278
    }
279
280
    /**
281
     * Fetches the localization for a given record.
282
     *
283
     * @param string $table Table name present in $GLOBALS['TCA']
284
     * @param int $uid The uid of the record
285
     * @param int $language The uid of the language record in sys_language
286
     * @param string $andWhereClause Optional additional WHERE clause (default: '')
287
     * @return mixed Multidimensional array with selected records, empty array if none exists and FALSE if table is not localizable
288
     */
289
    public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
290
    {
291
        $recordLocalization = false;
292
293
        if (self::isTableLocalizable($table)) {
294
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
295
296
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
297
                ->getQueryBuilderForTable($table);
298
            $queryBuilder->getRestrictions()
299
                ->removeAll()
300
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
301
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace ?? 0));
302
303
            $queryBuilder->select('*')
304
                ->from($table)
305
                ->where(
306
                    $queryBuilder->expr()->eq(
307
                        $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'],
308
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
309
                    ),
310
                    $queryBuilder->expr()->eq(
311
                        $tcaCtrl['languageField'],
312
                        $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
313
                    )
314
                )
315
                ->setMaxResults(1);
316
317
            if ($andWhereClause) {
318
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
319
            }
320
321
            $recordLocalization = $queryBuilder->execute()->fetchAll();
322
        }
323
324
        return $recordLocalization;
325
    }
326
327
    /*******************************************
328
     *
329
     * Page tree, TCA related
330
     *
331
     *******************************************/
332
    /**
333
     * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id
334
     * ($uid) and back to the root.
335
     * By default deleted pages are filtered.
336
     * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known
337
     * from the frontend where the rootline stops when a root-template is found.
338
     *
339
     * @param int $uid Page id for which to create the root line.
340
     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that
341
     *          stops the process if we meet a page, the user has no reading access to.
342
     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is
343
     *          usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
344
     * @param string[] $additionalFields Additional Fields to select for rootline records
345
     * @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
346
     */
347
    public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = [])
348
    {
349
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
350
        $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: [];
351
        $output = [];
352
        $pid = $uid;
353
        $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . md5(implode(',', $additionalFields)) : '');
354
        if (is_array($beGetRootLineCache[$ident] ?? false)) {
355
            $output = $beGetRootLineCache[$ident];
356
        } else {
357
            $loopCheck = 100;
358
            $theRowArray = [];
359
            while ($uid != 0 && $loopCheck) {
360
                $loopCheck--;
361
                $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields);
362
                if (is_array($row)) {
363
                    $uid = $row['pid'];
364
                    $theRowArray[] = $row;
365
                } else {
366
                    break;
367
                }
368
            }
369
            $fields = [
370
                'uid',
371
                'pid',
372
                'title',
373
                'doktype',
374
                'slug',
375
                'tsconfig_includes',
376
                'TSconfig',
377
                'is_siteroot',
378
                't3ver_oid',
379
                't3ver_wsid',
380
                't3ver_state',
381
                't3ver_stage',
382
                'backend_layout_next_level',
383
                'hidden',
384
                'starttime',
385
                'endtime',
386
                'fe_group',
387
                'nav_hide',
388
                'content_from_pid',
389
                'module',
390
                'extendToSubpages'
391
            ];
392
            $fields = array_merge($fields, $additionalFields);
393
            $rootPage = array_fill_keys($fields, null);
394
            if ($uid == 0) {
395
                $rootPage['uid'] = 0;
396
                $theRowArray[] = $rootPage;
397
            }
398
            $c = count($theRowArray);
399
            foreach ($theRowArray as $val) {
400
                $c--;
401
                $output[$c] = array_intersect_key($val, $rootPage);
402
                if (isset($val['_ORIG_pid'])) {
403
                    $output[$c]['_ORIG_pid'] = $val['_ORIG_pid'];
404
                }
405
            }
406
            $beGetRootLineCache[$ident] = $output;
407
            $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache);
408
        }
409
        return $output;
410
    }
411
412
    /**
413
     * Gets the cached page record for the rootline
414
     *
415
     * @param int $uid Page id for which to create the root line.
416
     * @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.
417
     * @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!
418
     * @param string[] $additionalFields AdditionalFields to fetch from the root line
419
     * @return array Cached page record for the rootline
420
     * @see BEgetRootLine
421
     */
422
    protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = [])
423
    {
424
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
425
        $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: [];
426
        $statementCacheIdent = md5($clause . ($additionalFields ? '-' . implode(',', $additionalFields) : ''));
427
        $ident = $uid . '-' . $workspaceOL . '-' . $statementCacheIdent;
428
        if (is_array($pageForRootlineCache[$ident] ?? false)) {
429
            $row = $pageForRootlineCache[$ident];
430
        } else {
431
            $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
432
            if (!$statement) {
433
                $queryBuilder = static::getQueryBuilderForTable('pages');
434
                $queryBuilder->getRestrictions()
435
                             ->removeAll()
436
                             ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
437
438
                $queryBuilder
439
                    ->select(
440
                        'pid',
441
                        'uid',
442
                        'title',
443
                        'doktype',
444
                        'slug',
445
                        'tsconfig_includes',
446
                        'TSconfig',
447
                        'is_siteroot',
448
                        't3ver_oid',
449
                        't3ver_wsid',
450
                        't3ver_state',
451
                        't3ver_stage',
452
                        'backend_layout_next_level',
453
                        'hidden',
454
                        'starttime',
455
                        'endtime',
456
                        'fe_group',
457
                        'nav_hide',
458
                        'content_from_pid',
459
                        'module',
460
                        'extendToSubpages',
461
                        ...$additionalFields
462
                    )
463
                    ->from('pages')
464
                    ->where(
465
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)),
466
                        QueryHelper::stripLogicalOperatorPrefix($clause)
467
                    );
468
                $statement = $queryBuilder->execute();
469
                if (class_exists(\Doctrine\DBAL\ForwardCompatibility\Result::class) && $statement instanceof \Doctrine\DBAL\ForwardCompatibility\Result) {
470
                    $statement = $statement->getIterator();
471
                }
472
                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement);
473
            } else {
474
                $statement->bindValue(1, (int)$uid);
475
                $statement->execute();
476
            }
477
            $row = $statement->fetch();
478
            $statement->closeCursor();
479
480
            if ($row) {
481
                if ($workspaceOL) {
482
                    self::workspaceOL('pages', $row);
483
                }
484
                if (is_array($row)) {
485
                    $pageForRootlineCache[$ident] = $row;
486
                    $runtimeCache->set('backendUtilityPageForRootLine', $pageForRootlineCache);
487
                }
488
            }
489
        }
490
        return $row;
491
    }
492
493
    /**
494
     * Opens the page tree to the specified page id
495
     *
496
     * @param int $pid Page id.
497
     * @param bool $clearExpansion If set, then other open branches are closed.
498
     * @internal should only be used from within TYPO3 Core
499
     */
500
    public static function openPageTree($pid, $clearExpansion)
501
    {
502
        $beUser = static::getBackendUserAuthentication();
503
        // Get current expansion data:
504
        if ($clearExpansion) {
505
            $expandedPages = [];
506
        } else {
507
            $expandedPages = $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'];
508
        }
509
        // Get rootline:
510
        $rL = self::BEgetRootLine($pid);
511
        // First, find out what mount index to use (if more than one DB mount exists):
512
        $mountIndex = 0;
513
        $mountKeys = $beUser->returnWebmounts();
514
515
        foreach ($rL as $rLDat) {
516
            if (isset($mountKeys[$rLDat['uid']])) {
517
                $mountIndex = $mountKeys[$rLDat['uid']];
518
                break;
519
            }
520
        }
521
        // Traverse rootline and open paths:
522
        foreach ($rL as $rLDat) {
523
            $expandedPages[$mountIndex . '_' . $rLDat['uid']] = '1';
524
        }
525
        // Write back:
526
        $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'] = $expandedPages;
527
        $beUser->writeUC();
528
    }
529
530
    /**
531
     * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage"
532
     * Each part of the path will be limited to $titleLimit characters
533
     * Deleted pages are filtered out.
534
     *
535
     * @param int $uid Page uid for which to create record path
536
     * @param string $clause Clause is additional where clauses, eg.
537
     * @param int $titleLimit Title limit
538
     * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so)
539
     * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set.
540
     */
541
    public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0)
542
    {
543
        if (!$titleLimit) {
544
            $titleLimit = 1000;
545
        }
546
        $output = $fullOutput = '/';
547
        $clause = trim($clause);
548
        if ($clause !== '' && strpos($clause, 'AND') !== 0) {
549
            $clause = 'AND ' . $clause;
550
        }
551
        $data = self::BEgetRootLine($uid, $clause, true);
552
        foreach ($data as $record) {
553
            if ($record['uid'] === 0) {
554
                continue;
555
            }
556
            $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output;
557
            if ($fullTitleLimit) {
558
                $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput;
559
            }
560
        }
561
        if ($fullTitleLimit) {
562
            return [$output, $fullOutput];
563
        }
564
        return $output;
565
    }
566
567
    /**
568
     * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA'].
569
     *
570
     * @param string $table The table to check
571
     * @return bool Whether a table is localizable
572
     */
573
    public static function isTableLocalizable($table)
574
    {
575
        $isLocalizable = false;
576
        if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) {
577
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
578
            $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField'];
579
        }
580
        return $isLocalizable;
581
    }
582
583
    /**
584
     * 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.
585
     * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin.
586
     * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause)
587
     *
588
     * @param int $id Page uid for which to check read-access
589
     * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1);
590
     * @return array|false Returns page record if OK, otherwise FALSE.
591
     */
592
    public static function readPageAccess($id, $perms_clause)
593
    {
594
        if ((string)$id !== '') {
595
            $id = (int)$id;
596
            if (!$id) {
597
                if (static::getBackendUserAuthentication()->isAdmin()) {
598
                    return ['_thePath' => '/'];
599
                }
600
            } else {
601
                $pageinfo = self::getRecord('pages', $id, '*', $perms_clause);
602
                if (($pageinfo['uid'] ?? false) && static::getBackendUserAuthentication()->isInWebMount($pageinfo, $perms_clause)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getBackendUserAu...ageinfo, $perms_clause) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

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

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

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

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

1161
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . /** @scrutinizer ignore-type */ $label;
Loading history...
1162
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) {
1163
            if ((int)$row['mount_pid'] > 0) {
1164
                if ($perms_clause) {
1165
                    $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20);
1166
                } else {
1167
                    $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title');
1168
                    $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')';
1169
                }
1170
                $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label;
1171
                if ($row['mount_pid_ol'] ?? 0) {
1172
                    $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']);
1173
                }
1174
            } else {
1175
                $parts[] = $lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:no_mount_pid');
1176
            }
1177
        }
1178
        if ($row['nav_hide']) {
1179
            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide');
1180
        }
1181
        if ($row['hidden']) {
1182
            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden');
1183
        }
1184
        if ($row['starttime']) {
1185
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label'])
1186
                . ' ' . self::dateTimeAge($row['starttime'], -1, 'date');
1187
        }
1188
        if ($row['endtime']) {
1189
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' '
1190
                . self::dateTimeAge($row['endtime'], -1, 'date');
1191
        }
1192
        if ($row['fe_group']) {
1193
            $fe_groups = [];
1194
            foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) {
1195
                if ($fe_group < 0) {
1196
                    $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', (string)$fe_group));
1197
                } else {
1198
                    $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title');
1199
                    $fe_groups[] = $lRec['title'];
1200
                }
1201
            }
1202
            $label = implode(', ', $fe_groups);
1203
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label;
1204
        }
1205
        $out = htmlspecialchars(implode(' - ', $parts));
1206
        return $includeAttrib ? 'title="' . $out . '"' : $out;
1207
    }
1208
1209
    /**
1210
     * Returns the combined markup for Bootstraps tooltips
1211
     *
1212
     * @param array $row
1213
     * @param string $table
1214
     * @return string
1215
     */
1216
    public static function getRecordToolTip(array $row, $table = 'pages')
1217
    {
1218
        $toolTipText = self::getRecordIconAltText($row, $table);
1219
        $toolTipCode = 'data-bs-toggle="tooltip" title=" '
1220
            . str_replace(' - ', '<br>', $toolTipText)
1221
            . '" data-bs-html="true" data-bs-placement="right"';
1222
        return $toolTipCode;
1223
    }
1224
1225
    /**
1226
     * Returns title-attribute information for ANY record (from a table defined in TCA of course)
1227
     * 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.
1228
     * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page.
1229
     *
1230
     * @param array $row Table row; $row is a row from the table, $table
1231
     * @param string $table Table name
1232
     * @return string
1233
     */
1234
    public static function getRecordIconAltText($row, $table = 'pages')
1235
    {
1236
        if ($table === 'pages') {
1237
            $out = self::titleAttribForPages($row, '', false);
1238
        } else {
1239
            $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'] ?? ''))
1240
                ? ($row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] ?? '') . ' '
1241
                : '';
1242
            $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'] ?? [];
1243
            // Uid is added
1244
            $out .= 'id=' . ($row['uid'] ?? 0);
1245
            if (static::isTableWorkspaceEnabled($table)) {
1246
                switch (VersionState::cast($row['t3ver_state'] ?? null)) {
1247
                    case new VersionState(VersionState::DELETE_PLACEHOLDER):
1248
                        $out .= ' - Deleted element!';
1249
                        break;
1250
                    case new VersionState(VersionState::MOVE_POINTER):
1251
                        $out .= ' - NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1252
                        break;
1253
                    case new VersionState(VersionState::NEW_PLACEHOLDER):
1254
                        $out .= ' - New element!';
1255
                        break;
1256
                }
1257
            }
1258
            // Hidden
1259
            $lang = static::getLanguageService();
1260
            if ($ctrl['disabled'] ?? false) {
1261
                $out .= ($row[$ctrl['disabled']] ?? false) ? ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') : '';
1262
            }
1263
            if (($ctrl['starttime'] ?? false) && ($row[$ctrl['starttime']] ?? 0) > $GLOBALS['EXEC_TIME']) {
1264
                $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') . ')';
1265
            }
1266
            if (($ctrl['endtime'] ?? false) && ($row[$ctrl['endtime']] ?? false)) {
1267
                $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') . ')';
1268
            }
1269
        }
1270
        return htmlspecialchars($out);
1271
    }
1272
1273
    /**
1274
     * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key
1275
     *
1276
     * @param string $table Table name, present in $GLOBALS['TCA']
1277
     * @param string $col Field name, present in $GLOBALS['TCA']
1278
     * @param string $key items-array value to match
1279
     * @return string Label for item entry
1280
     */
1281
    public static function getLabelFromItemlist($table, $col, $key)
1282
    {
1283
        // Check, if there is an "items" array:
1284
        if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) {
1285
            // Traverse the items-array...
1286
            foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) {
1287
                // ... and return the first found label where the value was equal to $key
1288
                if ((string)$v[1] === (string)$key) {
1289
                    return $v[0];
1290
                }
1291
            }
1292
        }
1293
        return '';
1294
    }
1295
1296
    /**
1297
     * Return the label of a field by additionally checking TsConfig values
1298
     *
1299
     * @param int $pageId Page id
1300
     * @param string $table Table name
1301
     * @param string $column Field Name
1302
     * @param string $key item value
1303
     * @return string Label for item entry
1304
     */
1305
    public static function getLabelFromItemListMerged($pageId, $table, $column, $key)
1306
    {
1307
        $pageTsConfig = static::getPagesTSconfig($pageId);
1308
        $label = '';
1309
        if (isset($pageTsConfig['TCEFORM.'])
1310
            && is_array($pageTsConfig['TCEFORM.'] ?? null)
1311
            && is_array($pageTsConfig['TCEFORM.'][$table . '.'] ?? null)
1312
            && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.'] ?? null)
1313
        ) {
1314
            if (is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'] ?? null)
1315
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key])
1316
            ) {
1317
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key];
1318
            } elseif (is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'] ?? null)
1319
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key])
1320
            ) {
1321
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key];
1322
            }
1323
        }
1324
        if (empty($label)) {
1325
            $tcaValue = self::getLabelFromItemlist($table, $column, $key);
1326
            if (!empty($tcaValue)) {
1327
                $label = $tcaValue;
1328
            }
1329
        }
1330
        return $label;
1331
    }
1332
1333
    /**
1334
     * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma.
1335
     * NOTE: this does not take itemsProcFunc into account
1336
     *
1337
     * @param string $table Table name, present in TCA
1338
     * @param string $column Field name
1339
     * @param string $keyList Key or comma-separated list of keys.
1340
     * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>)
1341
     * @return string Comma-separated list of localized labels
1342
     */
1343
    public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = [])
1344
    {
1345
        // Check if there is an "items" array
1346
        if (
1347
            !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1348
            || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1349
            || $keyList === ''
1350
        ) {
1351
            return '';
1352
        }
1353
1354
        $keys = GeneralUtility::trimExplode(',', $keyList, true);
1355
        $labels = [];
1356
        // Loop on all selected values
1357
        foreach ($keys as $key) {
1358
            $label = null;
1359
            if ($columnTsConfig) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columnTsConfig of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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