Completed
Push — master ( 8e305d...1d0f8f )
by
unknown
30:04
created

BackendUtility::versioningPlaceholderClause()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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\EventDispatcher\EventDispatcherInterface;
19
use Psr\Log\LoggerInterface;
20
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
21
use TYPO3\CMS\Backend\Controller\File\ThumbnailController;
22
use TYPO3\CMS\Backend\Routing\UriBuilder;
23
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
24
use TYPO3\CMS\Core\Cache\CacheManager;
25
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
26
use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent;
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\BackendWorkspaceRestriction;
37
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
38
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
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\Http\Uri;
43
use TYPO3\CMS\Core\Imaging\Icon;
44
use TYPO3\CMS\Core\Imaging\IconFactory;
45
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
46
use TYPO3\CMS\Core\Information\Typo3Information;
47
use TYPO3\CMS\Core\Localization\LanguageService;
48
use TYPO3\CMS\Core\Log\LogManager;
49
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
50
use TYPO3\CMS\Core\Resource\ProcessedFile;
51
use TYPO3\CMS\Core\Resource\ResourceFactory;
52
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
53
use TYPO3\CMS\Core\Routing\RouterInterface;
54
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
55
use TYPO3\CMS\Core\Site\SiteFinder;
56
use TYPO3\CMS\Core\Type\Bitmask\Permission;
57
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
58
use TYPO3\CMS\Core\Utility\ArrayUtility;
59
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
60
use TYPO3\CMS\Core\Utility\GeneralUtility;
61
use TYPO3\CMS\Core\Utility\HttpUtility;
62
use TYPO3\CMS\Core\Utility\MathUtility;
63
use TYPO3\CMS\Core\Utility\PathUtility;
64
use TYPO3\CMS\Core\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 $uid UID of record
90
     * @param string $fields List of fields to select
91
     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
92
     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
93
     * @return array|null Returns the row if found, otherwise NULL
94
     */
95
    public static function getRecord($table, $uid, $fields = '*', $where = '', $useDeleteClause = true)
96
    {
97
        // Ensure we have a valid uid (not 0 and not NEWxxxx) and a valid TCA
98
        if ((int)$uid && !empty($GLOBALS['TCA'][$table])) {
99
            $queryBuilder = static::getQueryBuilderForTable($table);
100
101
            // do not use enabled fields here
102
            $queryBuilder->getRestrictions()->removeAll();
103
104
            // should the delete clause be used
105
            if ($useDeleteClause) {
106
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
107
            }
108
109
            // set table and where clause
110
            $queryBuilder
111
                ->select(...GeneralUtility::trimExplode(',', $fields, true))
112
                ->from($table)
113
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)));
114
115
            // add custom where clause
116
            if ($where) {
117
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
118
            }
119
120
            $row = $queryBuilder->execute()->fetch();
121
            if ($row) {
122
                return $row;
123
            }
124
        }
125
        return null;
126
    }
127
128
    /**
129
     * Like getRecord(), but overlays workspace version if any.
130
     *
131
     * @param string $table Table name present in $GLOBALS['TCA']
132
     * @param int $uid UID of record
133
     * @param string $fields List of fields to select
134
     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
135
     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
136
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
137
     * @return array Returns the row if found, otherwise nothing
138
     */
139
    public static function getRecordWSOL(
140
        $table,
141
        $uid,
142
        $fields = '*',
143
        $where = '',
144
        $useDeleteClause = true,
145
        $unsetMovePointers = false
146
    ) {
147
        if ($fields !== '*') {
148
            $internalFields = GeneralUtility::uniqueList($fields . ',uid,pid');
149
            $row = self::getRecord($table, $uid, $internalFields, $where, $useDeleteClause);
150
            self::workspaceOL($table, $row, -99, $unsetMovePointers);
151
            if (is_array($row)) {
152
                foreach ($row as $key => $_) {
153
                    if (!GeneralUtility::inList($fields, $key) && $key[0] !== '_') {
154
                        unset($row[$key]);
155
                    }
156
                }
157
            }
158
        } else {
159
            $row = self::getRecord($table, $uid, $fields, $where, $useDeleteClause);
160
            self::workspaceOL($table, $row, -99, $unsetMovePointers);
161
        }
162
        return $row;
163
    }
164
165
    /**
166
     * Purges computed properties starting with underscore character ('_').
167
     *
168
     * @param array $record
169
     * @return array
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)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::splitTable_Uid" is not in camel caps format
Loading history...
209
    {
210
        [$uid, $table] = explode('_', strrev($str), 2);
211
        return [strrev($table), strrev($uid)];
212
    }
213
214
    /**
215
     * Backend implementation of enableFields()
216
     * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime.
217
     * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition.
218
     * $GLOBALS["SIM_ACCESS_TIME"] is used for date.
219
     *
220
     * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA'].
221
     * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection)
222
     * @return string WHERE clause part
223
     * @internal should only be used from within TYPO3 Core, but DefaultRestrictionHandler is recommended as alternative
224
     */
225
    public static function BEenableFields($table, $inv = false)
226
    {
227
        $ctrl = $GLOBALS['TCA'][$table]['ctrl'];
228
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
229
            ->getConnectionForTable($table)
230
            ->getExpressionBuilder();
231
        $query = $expressionBuilder->andX();
232
        $invQuery = $expressionBuilder->orX();
233
234
        if (is_array($ctrl)) {
235
            if (is_array($ctrl['enablecolumns'])) {
236
                if ($ctrl['enablecolumns']['disabled'] ?? false) {
237
                    $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
238
                    $query->add($expressionBuilder->eq($field, 0));
239
                    $invQuery->add($expressionBuilder->neq($field, 0));
240
                }
241
                if ($ctrl['enablecolumns']['starttime'] ?? false) {
242
                    $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
243
                    $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']));
244
                    $invQuery->add(
245
                        $expressionBuilder->andX(
246
                            $expressionBuilder->neq($field, 0),
247
                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
248
                        )
249
                    );
250
                }
251
                if ($ctrl['enablecolumns']['endtime'] ?? false) {
252
                    $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
253
                    $query->add(
254
                        $expressionBuilder->orX(
255
                            $expressionBuilder->eq($field, 0),
256
                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
257
                        )
258
                    );
259
                    $invQuery->add(
260
                        $expressionBuilder->andX(
261
                            $expressionBuilder->neq($field, 0),
262
                            $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
263
                        )
264
                    );
265
                }
266
            }
267
        }
268
269
        if ($query->count() === 0) {
270
            return '';
271
        }
272
273
        return ' AND ' . ($inv ? $invQuery : $query);
274
    }
275
276
    /**
277
     * Fetches the localization for a given record.
278
     *
279
     * @param string $table Table name present in $GLOBALS['TCA']
280
     * @param int $uid The uid of the record
281
     * @param int $language The uid of the language record in sys_language
282
     * @param string $andWhereClause Optional additional WHERE clause (default: '')
283
     * @return mixed Multidimensional array with selected records, empty array if none exists and FALSE if table is not localizable
284
     */
285
    public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
286
    {
287
        $recordLocalization = false;
288
289
        if (self::isTableLocalizable($table)) {
290
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
291
292
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
293
                ->getQueryBuilderForTable($table);
294
            $queryBuilder->getRestrictions()
295
                ->removeAll()
296
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
297
                ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
298
299
            $queryBuilder->select('*')
300
                ->from($table)
301
                ->where(
302
                    $queryBuilder->expr()->eq(
303
                        $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'],
304
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
305
                    ),
306
                    $queryBuilder->expr()->eq(
307
                        $tcaCtrl['languageField'],
308
                        $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
309
                    )
310
                )
311
                ->setMaxResults(1);
312
313
            if ($andWhereClause) {
314
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
315
            }
316
317
            $recordLocalization = $queryBuilder->execute()->fetchAll();
318
        }
319
320
        return $recordLocalization;
321
    }
322
323
    /*******************************************
324
     *
325
     * Page tree, TCA related
326
     *
327
     *******************************************/
328
    /**
329
     * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id
330
     * ($uid) and back to the root.
331
     * By default deleted pages are filtered.
332
     * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known
333
     * from the frontend where the rootline stops when a root-template is found.
334
     *
335
     * @param int $uid Page id for which to create the root line.
336
     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that
337
     *          stops the process if we meet a page, the user has no reading access to.
338
     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is
339
     *          usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
340
     * @param string[] $additionalFields Additional Fields to select for rootline records
341
     * @return array Root line array, all the way to the page tree root (or as far as $clause allows!)
342
     */
343
    public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = [])
344
    {
345
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
346
        $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: [];
347
        $output = [];
348
        $pid = $uid;
349
        $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . md5(implode(',', $additionalFields)) : '');
350
        if (is_array($beGetRootLineCache[$ident] ?? false)) {
351
            $output = $beGetRootLineCache[$ident];
352
        } else {
353
            $loopCheck = 100;
354
            $theRowArray = [];
355
            while ($uid != 0 && $loopCheck) {
356
                $loopCheck--;
357
                $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields);
358
                if (is_array($row)) {
359
                    $uid = $row['pid'];
360
                    $theRowArray[] = $row;
361
                } else {
362
                    break;
363
                }
364
            }
365
            $fields = [
366
                'uid',
367
                'pid',
368
                'title',
369
                'doktype',
370
                'slug',
371
                'tsconfig_includes',
372
                'TSconfig',
373
                'is_siteroot',
374
                't3ver_oid',
375
                't3ver_wsid',
376
                't3ver_state',
377
                't3ver_stage',
378
                'backend_layout_next_level',
379
                'hidden',
380
                'starttime',
381
                'endtime',
382
                'fe_group',
383
                'nav_hide',
384
                'content_from_pid',
385
                'module',
386
                'extendToSubpages'
387
            ];
388
            $fields = array_merge($fields, $additionalFields);
389
            $rootPage = array_fill_keys($fields, null);
390
            if ($uid == 0) {
391
                $rootPage['uid'] = 0;
392
                $theRowArray[] = $rootPage;
393
            }
394
            $c = count($theRowArray);
395
            foreach ($theRowArray as $val) {
396
                $c--;
397
                $output[$c] = array_intersect_key($val, $rootPage);
398
                if (isset($val['_ORIG_pid'])) {
399
                    $output[$c]['_ORIG_pid'] = $val['_ORIG_pid'];
400
                }
401
            }
402
            $beGetRootLineCache[$ident] = $output;
403
            $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache);
404
        }
405
        return $output;
406
    }
407
408
    /**
409
     * Gets the cached page record for the rootline
410
     *
411
     * @param int $uid Page id for which to create the root line.
412
     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that stops the process if we meet a page, the user has no reading access to.
413
     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
414
     * @param string[] $additionalFields AdditionalFields to fetch from the root line
415
     * @return array Cached page record for the rootline
416
     * @see BEgetRootLine
417
     */
418
    protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = [])
419
    {
420
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
421
        $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: [];
422
        $statementCacheIdent = md5($clause . ($additionalFields ? '-' . implode(',', $additionalFields) : ''));
423
        $ident = $uid . '-' . $workspaceOL . '-' . $statementCacheIdent;
424
        if (is_array($pageForRootlineCache[$ident] ?? false)) {
425
            $row = $pageForRootlineCache[$ident];
426
        } else {
427
            $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
428
            if (!$statement) {
429
                $queryBuilder = static::getQueryBuilderForTable('pages');
430
                $queryBuilder->getRestrictions()
431
                             ->removeAll()
432
                             ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
433
434
                $queryBuilder
435
                    ->select(
436
                        'pid',
437
                        'uid',
438
                        'title',
439
                        'doktype',
440
                        'slug',
441
                        'tsconfig_includes',
442
                        'TSconfig',
443
                        'is_siteroot',
444
                        't3ver_oid',
445
                        't3ver_wsid',
446
                        't3ver_state',
447
                        't3ver_stage',
448
                        'backend_layout_next_level',
449
                        'hidden',
450
                        'starttime',
451
                        'endtime',
452
                        'fe_group',
453
                        'nav_hide',
454
                        'content_from_pid',
455
                        'module',
456
                        'extendToSubpages',
457
                        ...$additionalFields
458
                    )
459
                    ->from('pages')
460
                    ->where(
461
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)),
462
                        QueryHelper::stripLogicalOperatorPrefix($clause)
463
                    );
464
                $statement = $queryBuilder->execute();
465
                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement);
466
            } else {
467
                $statement->bindValue(1, (int)$uid);
468
                $statement->execute();
469
            }
470
            $row = $statement->fetch();
471
            $statement->closeCursor();
472
473
            if ($row) {
474
                $newLocation = false;
475
                if ($workspaceOL) {
476
                    self::workspaceOL('pages', $row);
477
                    $newLocation = self::getMovePlaceholder('pages', $row['uid'], 'pid');
478
                }
479
                if (is_array($row)) {
480
                    if ($newLocation !== false) {
481
                        $row['pid'] = $newLocation['pid'];
482
                    } else {
483
                        self::fixVersioningPid('pages', $row);
484
                    }
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 = json_decode($beUser->uc['browseTrees']['browsePages'], true);
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 = array_flip($beUser->returnWebmounts());
514
        foreach ($rL as $rLDat) {
515
            if (isset($mountKeys[$rLDat['uid']])) {
516
                $mountIndex = $mountKeys[$rLDat['uid']];
517
                break;
518
            }
519
        }
520
        // Traverse rootline and open paths:
521
        foreach ($rL as $rLDat) {
522
            $expandedPages[$mountIndex][$rLDat['uid']] = 1;
523
        }
524
        // Write back:
525
        $beUser->uc['browseTrees']['browsePages'] = json_encode($expandedPages);
526
        $beUser->writeUC();
527
    }
528
529
    /**
530
     * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage"
531
     * Each part of the path will be limited to $titleLimit characters
532
     * Deleted pages are filtered out.
533
     *
534
     * @param int $uid Page uid for which to create record path
535
     * @param string $clause Clause is additional where clauses, eg.
536
     * @param int $titleLimit Title limit
537
     * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so)
538
     * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set.
539
     */
540
    public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0)
541
    {
542
        if (!$titleLimit) {
543
            $titleLimit = 1000;
544
        }
545
        $output = $fullOutput = '/';
546
        $clause = trim($clause);
547
        if ($clause !== '' && strpos($clause, 'AND') !== 0) {
548
            $clause = 'AND ' . $clause;
549
        }
550
        $data = self::BEgetRootLine($uid, $clause, true);
551
        foreach ($data as $record) {
552
            if ($record['uid'] === 0) {
553
                continue;
554
            }
555
            $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output;
556
            if ($fullTitleLimit) {
557
                $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput;
558
            }
559
        }
560
        if ($fullTitleLimit) {
561
            return [$output, $fullOutput];
562
        }
563
        return $output;
564
    }
565
566
    /**
567
     * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA'].
568
     *
569
     * @param string $table The table to check
570
     * @return bool Whether a table is localizable
571
     */
572
    public static function isTableLocalizable($table)
573
    {
574
        $isLocalizable = false;
575
        if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) {
576
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
577
            $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField'];
578
        }
579
        return $isLocalizable;
580
    }
581
582
    /**
583
     * 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.
584
     * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin.
585
     * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause)
586
     *
587
     * @param int $id Page uid for which to check read-access
588
     * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1);
589
     * @return array|bool Returns page record if OK, otherwise FALSE.
590
     */
591
    public static function readPageAccess($id, $perms_clause)
592
    {
593
        if ((string)$id !== '') {
594
            $id = (int)$id;
595
            if (!$id) {
596
                if (static::getBackendUserAuthentication()->isAdmin()) {
597
                    return ['_thePath' => '/'];
598
                }
599
            } else {
600
                $pageinfo = self::getRecord('pages', $id, '*', $perms_clause);
601
                if ($pageinfo['uid'] && 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...
602
                    self::workspaceOL('pages', $pageinfo);
603
                    if (is_array($pageinfo)) {
604
                        self::fixVersioningPid('pages', $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]) {
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]) {
660
                        $typeNum = $foreignRow[$foreignTableTypeField];
661
                    }
662
                }
663
            } else {
664
                $typeNum = $row[$field];
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(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
     * Returns the non-parsed Page TSconfig for page with id, $id
749
     *
750
     * @param int $id Page uid for which to create Page TSconfig
751
     * @param array $rootLine If $rootLine is an array, that is used as rootline, otherwise rootline is just calculated
752
     * @return array Non-parsed Page TSconfig
753
     */
754
    public static function getRawPagesTSconfig($id, array $rootLine = null)
755
    {
756
        trigger_error('BackendUtility::getRawPagesTSconfig will be removed in TYPO3 v11.0. Use PageTsConfigLoader instead.', E_USER_DEPRECATED);
757
        if (!is_array($rootLine)) {
758
            $rootLine = self::BEgetRootLine($id, '', true);
759
        }
760
761
        // Order correctly
762
        ksort($rootLine);
763
        $tsDataArray = [];
764
        // Setting default configuration
765
        $tsDataArray['defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
766
        foreach ($rootLine as $k => $v) {
767
            if (trim($v['tsconfig_includes'])) {
768
                $includeTsConfigFileList = GeneralUtility::trimExplode(',', $v['tsconfig_includes'], true);
769
                // Traversing list
770
                foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
771
                    if (strpos($includeTsConfigFile, 'EXT:') === 0) {
772
                        [$includeTsConfigFileExtensionKey, $includeTsConfigFilename] = explode(
773
                            '/',
774
                            substr($includeTsConfigFile, 4),
775
                            2
776
                        );
777
                        if ((string)$includeTsConfigFileExtensionKey !== ''
778
                            && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
779
                            && (string)$includeTsConfigFilename !== ''
780
                        ) {
781
                            $extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey);
782
                            $includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename);
783
                            if (strpos($includeTsConfigFileAndPath, $extensionPath) === 0 && file_exists($includeTsConfigFileAndPath)) {
784
                                $tsDataArray['uid_' . $v['uid'] . '_static_' . $key] = file_get_contents($includeTsConfigFileAndPath);
785
                            }
786
                        }
787
                    }
788
                }
789
            }
790
            $tsDataArray['uid_' . $v['uid']] = $v['TSconfig'];
791
        }
792
793
        $eventDispatcher = GeneralUtility::getContainer()->get(EventDispatcherInterface::class);
794
        $event = $eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent($tsDataArray, $rootLine));
795
        return TypoScriptParser::checkIncludeLines_array($event->getTsConfig());
796
    }
797
798
    /*******************************************
799
     *
800
     * Users / Groups related
801
     *
802
     *******************************************/
803
    /**
804
     * Returns an array with be_users records of all user NOT DELETED sorted by their username
805
     * Keys in the array is the be_users uid
806
     *
807
     * @param string $fields Optional $fields list (default: username,usergroup,usergroup_cached_list,uid) can be used to set the selected fields
808
     * @param string $where Optional $where clause (fx. "AND username='pete'") can be used to limit query
809
     * @return array
810
     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
811
     */
812
    public static function getUserNames($fields = 'username,usergroup,usergroup_cached_list,uid', $where = '')
813
    {
814
        return self::getRecordsSortedByTitle(
815
            GeneralUtility::trimExplode(',', $fields, true),
816
            'be_users',
817
            'username',
818
            'AND pid=0 ' . $where
819
        );
820
    }
821
822
    /**
823
     * Returns an array with be_groups records (title, uid) of all groups NOT DELETED sorted by their title
824
     *
825
     * @param string $fields Field list
826
     * @param string $where WHERE clause
827
     * @return array
828
     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
829
     */
830
    public static function getGroupNames($fields = 'title,uid', $where = '')
831
    {
832
        return self::getRecordsSortedByTitle(
833
            GeneralUtility::trimExplode(',', $fields, true),
834
            'be_groups',
835
            'title',
836
            'AND pid=0 ' . $where
837
        );
838
    }
839
840
    /**
841
     * Returns an array of all non-deleted records of a table sorted by a given title field.
842
     * The value of the title field will be replaced by the return value
843
     * of self::getRecordTitle() before the sorting is performed.
844
     *
845
     * @param array $fields Fields to select
846
     * @param string $table Table name
847
     * @param string $titleField Field that will contain the record title
848
     * @param string $where Additional where clause
849
     * @return array Array of sorted records
850
     */
851
    protected static function getRecordsSortedByTitle(array $fields, $table, $titleField, $where = '')
852
    {
853
        $fieldsIndex = array_flip($fields);
854
        // Make sure the titleField is amongst the fields when getting sorted
855
        $fieldsIndex[$titleField] = 1;
856
857
        $result = [];
858
859
        $queryBuilder = static::getQueryBuilderForTable($table);
860
        $queryBuilder->getRestrictions()
861
            ->removeAll()
862
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
863
864
        $res = $queryBuilder
865
            ->select('*')
866
            ->from($table)
867
            ->where(QueryHelper::stripLogicalOperatorPrefix($where))
868
            ->execute();
869
870
        while ($record = $res->fetch()) {
871
            // store the uid, because it might be unset if it's not among the requested $fields
872
            $recordId = $record['uid'];
873
            $record[$titleField] = self::getRecordTitle($table, $record);
874
875
            // include only the requested fields in the result
876
            $result[$recordId] = array_intersect_key($record, $fieldsIndex);
877
        }
878
879
        // sort records by $sortField. This is not done in the query because the title might have been overwritten by
880
        // self::getRecordTitle();
881
        return ArrayUtility::sortArraysByKey($result, $titleField);
882
    }
883
884
    /**
885
     * Returns the array $usernames with the names of all users NOT IN $groupArray changed to the uid (hides the usernames!).
886
     * If $excludeBlindedFlag is set, then these records are unset from the array $usernames
887
     * Takes $usernames (array made by \TYPO3\CMS\Backend\Utility\BackendUtility::getUserNames()) and a $groupArray (array with the groups a certain user is member of) as input
888
     *
889
     * @param array $usernames User names
890
     * @param array $groupArray Group names
891
     * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
892
     * @return array User names, blinded
893
     * @internal
894
     */
895
    public static function blindUserNames($usernames, $groupArray, $excludeBlindedFlag = false)
896
    {
897
        if (is_array($usernames) && is_array($groupArray)) {
0 ignored issues
show
introduced by
The condition is_array($groupArray) is always true.
Loading history...
898
            foreach ($usernames as $uid => $row) {
899
                $userN = $uid;
900
                $set = 0;
901
                if ($row['uid'] != static::getBackendUserAuthentication()->user['uid']) {
902
                    foreach ($groupArray as $v) {
903
                        if ($v && GeneralUtility::inList($row['usergroup_cached_list'], $v)) {
904
                            $userN = $row['username'];
905
                            $set = 1;
906
                        }
907
                    }
908
                } else {
909
                    $userN = $row['username'];
910
                    $set = 1;
911
                }
912
                $usernames[$uid]['username'] = $userN;
913
                if ($excludeBlindedFlag && !$set) {
914
                    unset($usernames[$uid]);
915
                }
916
            }
917
        }
918
        return $usernames;
919
    }
920
921
    /**
922
     * Corresponds to blindUserNames but works for groups instead
923
     *
924
     * @param array $groups Group names
925
     * @param array $groupArray Group names (reference)
926
     * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
927
     * @return array
928
     * @internal
929
     */
930
    public static function blindGroupNames($groups, $groupArray, $excludeBlindedFlag = false)
931
    {
932
        if (is_array($groups) && is_array($groupArray)) {
0 ignored issues
show
introduced by
The condition is_array($groupArray) is always true.
Loading history...
933
            foreach ($groups as $uid => $row) {
934
                $groupN = $uid;
935
                $set = 0;
936
                if (in_array($uid, $groupArray, false)) {
937
                    $groupN = $row['title'];
938
                    $set = 1;
939
                }
940
                $groups[$uid]['title'] = $groupN;
941
                if ($excludeBlindedFlag && !$set) {
942
                    unset($groups[$uid]);
943
                }
944
            }
945
        }
946
        return $groups;
947
    }
948
949
    /*******************************************
950
     *
951
     * Output related
952
     *
953
     *******************************************/
954
    /**
955
     * Returns the difference in days between input $tstamp and $EXEC_TIME
956
     *
957
     * @param int $tstamp Time stamp, seconds
958
     * @return int
959
     */
960
    public static function daysUntil($tstamp)
961
    {
962
        $delta_t = $tstamp - $GLOBALS['EXEC_TIME'];
963
        return ceil($delta_t / (3600 * 24));
964
    }
965
966
    /**
967
     * Returns $tstamp formatted as "ddmmyy" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'])
968
     *
969
     * @param int $tstamp Time stamp, seconds
970
     * @return string Formatted time
971
     */
972
    public static function date($tstamp)
973
    {
974
        return date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$tstamp);
975
    }
976
977
    /**
978
     * Returns $tstamp formatted as "ddmmyy hhmm" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] AND $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'])
979
     *
980
     * @param int $value Time stamp, seconds
981
     * @return string Formatted time
982
     */
983
    public static function datetime($value)
984
    {
985
        return date(
986
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
987
            $value
988
        );
989
    }
990
991
    /**
992
     * Returns $value (in seconds) formatted as hh:mm:ss
993
     * For instance $value = 3600 + 60*2 + 3 should return "01:02:03"
994
     *
995
     * @param int $value Time stamp, seconds
996
     * @param bool $withSeconds Output hh:mm:ss. If FALSE: hh:mm
997
     * @return string Formatted time
998
     */
999
    public static function time($value, $withSeconds = true)
1000
    {
1001
        return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value);
1002
    }
1003
1004
    /**
1005
     * Returns the "age" in minutes / hours / days / years of the number of $seconds inputted.
1006
     *
1007
     * @param int $seconds Seconds could be the difference of a certain timestamp and time()
1008
     * @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")
1009
     * @return string Formatted time
1010
     */
1011
    public static function calcAge($seconds, $labels = 'min|hrs|days|yrs|min|hour|day|year')
1012
    {
1013
        $labelArr = GeneralUtility::trimExplode('|', $labels, true);
1014
        $absSeconds = abs($seconds);
1015
        $sign = $seconds < 0 ? -1 : 1;
1016
        if ($absSeconds < 3600) {
1017
            $val = round($absSeconds / 60);
1018
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[4] : $labelArr[0]);
1019
        } elseif ($absSeconds < 24 * 3600) {
1020
            $val = round($absSeconds / 3600);
1021
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[5] : $labelArr[1]);
1022
        } elseif ($absSeconds < 365 * 24 * 3600) {
1023
            $val = round($absSeconds / (24 * 3600));
1024
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[6] : $labelArr[2]);
1025
        } else {
1026
            $val = round($absSeconds / (365 * 24 * 3600));
1027
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[7] : $labelArr[3]);
1028
        }
1029
        return $seconds;
1030
    }
1031
1032
    /**
1033
     * Returns a formatted timestamp if $tstamp is set.
1034
     * The date/datetime will be followed by the age in parenthesis.
1035
     *
1036
     * @param int $tstamp Time stamp, seconds
1037
     * @param int $prefix 1/-1 depending on polarity of age.
1038
     * @param string $date $date=="date" will yield "dd:mm:yy" formatting, otherwise "dd:mm:yy hh:mm
1039
     * @return string
1040
     */
1041
    public static function dateTimeAge($tstamp, $prefix = 1, $date = '')
1042
    {
1043
        if (!$tstamp) {
1044
            return '';
1045
        }
1046
        $label = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
1047
        $age = ' (' . self::calcAge($prefix * ($GLOBALS['EXEC_TIME'] - $tstamp), $label) . ')';
1048
        return ($date === 'date' ? self::date($tstamp) : self::datetime($tstamp)) . $age;
1049
    }
1050
1051
    /**
1052
     * Resolves file references for a given record.
1053
     *
1054
     * @param string $tableName Name of the table of the record
1055
     * @param string $fieldName Name of the field of the record
1056
     * @param array $element Record data
1057
     * @param int|null $workspaceId Workspace to fetch data for
1058
     * @return \TYPO3\CMS\Core\Resource\FileReference[]|null
1059
     */
1060
    public static function resolveFileReferences($tableName, $fieldName, $element, $workspaceId = null)
1061
    {
1062
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1063
            return null;
1064
        }
1065
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1066
        if (empty($configuration['type']) || $configuration['type'] !== 'inline'
1067
            || empty($configuration['foreign_table']) || $configuration['foreign_table'] !== 'sys_file_reference'
1068
        ) {
1069
            return null;
1070
        }
1071
1072
        $fileReferences = [];
1073
        /** @var RelationHandler $relationHandler */
1074
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1075
        if ($workspaceId !== null) {
1076
            $relationHandler->setWorkspaceId($workspaceId);
1077
        }
1078
        $relationHandler->start(
1079
            $element[$fieldName],
1080
            $configuration['foreign_table'],
1081
            $configuration['MM'] ?? '',
1082
            $element['uid'],
1083
            $tableName,
1084
            $configuration
1085
        );
1086
        $relationHandler->processDeletePlaceholder();
1087
        $referenceUids = $relationHandler->tableArray[$configuration['foreign_table']];
1088
1089
        foreach ($referenceUids as $referenceUid) {
1090
            try {
1091
                $fileReference = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject(
1092
                    $referenceUid,
1093
                    [],
1094
                    $workspaceId === 0
1095
                );
1096
                $fileReferences[$fileReference->getUid()] = $fileReference;
1097
            } catch (FileDoesNotExistException $e) {
1098
                /**
1099
                 * We just catch the exception here
1100
                 * Reasoning: There is nothing an editor or even admin could do
1101
                 */
1102
            } catch (\InvalidArgumentException $e) {
1103
                /**
1104
                 * The storage does not exist anymore
1105
                 * Log the exception message for admins as they maybe can restore the storage
1106
                 */
1107
                self::getLogger()->error($e->getMessage(), ['table' => $tableName, 'fieldName' => $fieldName, 'referenceUid' => $referenceUid, 'exception' => $e]);
1108
            }
1109
        }
1110
1111
        return $fileReferences;
1112
    }
1113
1114
    /**
1115
     * Returns a linked image-tag for thumbnail(s)/fileicons/truetype-font-previews from a database row with sys_file_references
1116
     * All $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] extension are made to thumbnails + ttf file (renders font-example)
1117
     * Thumbnails are linked to ShowItemController (/thumbnails route)
1118
     *
1119
     * @param array $row Row is the database row from the table, $table.
1120
     * @param string $table Table name for $row (present in TCA)
1121
     * @param string $field Field is pointing to the connecting field of sys_file_references
1122
     * @param string $backPath Back path prefix for image tag src="" field
1123
     * @param string $thumbScript UNUSED since FAL
1124
     * @param string $uploaddir UNUSED since FAL
1125
     * @param int $abs UNUSED
1126
     * @param string $tparams Optional: $tparams is additional attributes for the image tags
1127
     * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default.
1128
     * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup
1129
     * @return string Thumbnail image tag.
1130
     */
1131
    public static function thumbCode(
1132
        $row,
1133
        $table,
1134
        $field,
1135
        $backPath = '',
1136
        $thumbScript = '',
1137
        $uploaddir = null,
1138
        $abs = 0,
1139
        $tparams = '',
1140
        $size = '',
1141
        $linkInfoPopup = true
1142
    ) {
1143
        // Check and parse the size parameter
1144
        $size = trim($size);
1145
        $sizeParts = [64, 64];
1146
        if ($size) {
1147
            $sizeParts = explode('x', $size . 'x' . $size);
1148
        }
1149
        $thumbData = '';
1150
        $fileReferences = static::resolveFileReferences($table, $field, $row);
1151
        // FAL references
1152
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1153
        if ($fileReferences !== null) {
1154
            foreach ($fileReferences as $fileReferenceObject) {
1155
                // Do not show previews of hidden references
1156
                if ($fileReferenceObject->getProperty('hidden')) {
1157
                    continue;
1158
                }
1159
                $fileObject = $fileReferenceObject->getOriginalFile();
1160
1161
                if ($fileObject->isMissing()) {
1162
                    $thumbData .= '<span class="label label-danger">'
1163
                        . htmlspecialchars(
1164
                            static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')
1165
                        )
1166
                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1167
                    continue;
1168
                }
1169
1170
                // Preview web image or media elements
1171
                if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
1172
                    && GeneralUtility::inList(
1173
                        $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
1174
                        $fileReferenceObject->getExtension()
1175
                    )
1176
                ) {
1177
                    $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
1178
                    $cropArea = $cropVariantCollection->getCropArea();
1179
                    $imageUrl = self::getThumbnailUrl($fileObject->getUid(), [
1180
                        'width' => $sizeParts[0],
1181
                        'height' => $sizeParts[1] . 'c',
1182
                        'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
1183
                        '_context' => $cropArea->isEmpty() ? ProcessedFile::CONTEXT_IMAGEPREVIEW : ProcessedFile::CONTEXT_IMAGECROPSCALEMASK
1184
                    ]);
1185
                    $attributes = [
1186
                        'src' => $imageUrl,
1187
                        'width' => (int)$sizeParts[0],
1188
                        'height' => (int)$sizeParts[1],
1189
                        'alt' => $fileReferenceObject->getName(),
1190
                    ];
1191
                    $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . $tparams . '/>';
1192
                } else {
1193
                    // Icon
1194
                    $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1195
                        . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1196
                        . '</span>';
1197
                }
1198
                if ($linkInfoPopup) {
1199
                    // @todo Should we add requireJsModule again (should be loaded in most/all cases)
1200
                    // loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
1201
                    $attributes = GeneralUtility::implodeAttributes([
1202
                        'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
1203
                        'data-dispatch-args-list' => '_FILE,' . (int)$fileObject->getUid(),
1204
                    ], true);
1205
                    $thumbData .= '<a href="#" ' . $attributes . '>' . $imgTag . '</a> ';
1206
                } else {
1207
                    $thumbData .= $imgTag;
1208
                }
1209
            }
1210
        }
1211
        return $thumbData;
1212
    }
1213
1214
    /**
1215
     * @param int $fileId
1216
     * @param array $configuration
1217
     * @return string
1218
     * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
1219
     */
1220
    public static function getThumbnailUrl(int $fileId, array $configuration): string
1221
    {
1222
        $parameters = json_encode([
1223
            'fileId' => $fileId,
1224
            'configuration' => $configuration
1225
        ]);
1226
        $uriParameters = [
1227
            'parameters' => $parameters,
1228
            'hmac' => GeneralUtility::hmac(
1229
                $parameters,
1230
                ThumbnailController::class
1231
            ),
1232
        ];
1233
        return (string)GeneralUtility::makeInstance(UriBuilder::class)
1234
            ->buildUriFromRoute('thumbnails', $uriParameters);
1235
    }
1236
1237
    /**
1238
     * Returns title-attribute information for a page-record informing about id, doktype, hidden, starttime, endtime, fe_group etc.
1239
     *
1240
     * @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)
1241
     * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4)
1242
     * @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
1243
     * @return string
1244
     */
1245
    public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true)
1246
    {
1247
        $lang = static::getLanguageService();
1248
        $parts = [];
1249
        $parts[] = 'id=' . $row['uid'];
1250
        if ($row['uid'] === 0) {
1251
            $out = htmlspecialchars($parts[0]);
1252
            return $includeAttrib ? 'title="' . $out . '"' : $out;
1253
        }
1254
        switch (VersionState::cast($row['t3ver_state'])) {
1255
            case new VersionState(VersionState::NEW_PLACEHOLDER):
1256
                $parts[] = 'PLH WSID#' . $row['t3ver_wsid'];
1257
                break;
1258
            case new VersionState(VersionState::DELETE_PLACEHOLDER):
1259
                $parts[] = 'Deleted element!';
1260
                break;
1261
            case new VersionState(VersionState::MOVE_PLACEHOLDER):
1262
                $parts[] = 'OLD LOCATION (Move Placeholder) WSID#' . $row['t3ver_wsid'];
1263
                break;
1264
            case new VersionState(VersionState::MOVE_POINTER):
1265
                $parts[] = 'NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1266
                break;
1267
            case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1268
                $parts[] = 'New element!';
1269
                break;
1270
        }
1271
        if ($row['doktype'] == PageRepository::DOKTYPE_LINK) {
1272
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label']) . ' ' . $row['url'];
1273
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1274
            if ($perms_clause) {
1275
                $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20);
1276
            } else {
1277
                $row['shortcut'] = (int)$row['shortcut'];
1278
                $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title');
1279
                $label = $lRec['title'] . ' (id=' . $row['shortcut'] . ')';
1280
            }
1281
            if ($row['shortcut_mode'] != PageRepository::SHORTCUT_MODE_NONE) {
1282
                $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' '
1283
                    . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode']));
1284
            }
1285
            $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

1285
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . /** @scrutinizer ignore-type */ $label;
Loading history...
1286
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) {
1287
            if ((int)$row['mount_pid'] > 0) {
1288
                if ($perms_clause) {
1289
                    $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20);
1290
                } else {
1291
                    $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title');
1292
                    $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')';
1293
                }
1294
                $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label;
1295
                if ($row['mount_pid_ol']) {
1296
                    $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']);
1297
                }
1298
            } else {
1299
                $parts[] = $lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:no_mount_pid');
1300
            }
1301
        }
1302
        if ($row['nav_hide']) {
1303
            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide');
1304
        }
1305
        if ($row['hidden']) {
1306
            $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden');
1307
        }
1308
        if ($row['starttime']) {
1309
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label'])
1310
                . ' ' . self::dateTimeAge($row['starttime'], -1, 'date');
1311
        }
1312
        if ($row['endtime']) {
1313
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' '
1314
                . self::dateTimeAge($row['endtime'], -1, 'date');
1315
        }
1316
        if ($row['fe_group']) {
1317
            $fe_groups = [];
1318
            foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) {
1319
                if ($fe_group < 0) {
1320
                    $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', $fe_group));
1321
                } else {
1322
                    $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title');
1323
                    $fe_groups[] = $lRec['title'];
1324
                }
1325
            }
1326
            $label = implode(', ', $fe_groups);
1327
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label;
1328
        }
1329
        $out = htmlspecialchars(implode(' - ', $parts));
1330
        return $includeAttrib ? 'title="' . $out . '"' : $out;
1331
    }
1332
1333
    /**
1334
     * Returns the combined markup for Bootstraps tooltips
1335
     *
1336
     * @param array $row
1337
     * @param string $table
1338
     * @return string
1339
     */
1340
    public static function getRecordToolTip(array $row, $table = 'pages')
1341
    {
1342
        $toolTipText = self::getRecordIconAltText($row, $table);
1343
        $toolTipCode = 'data-toggle="tooltip" data-title=" '
1344
            . str_replace(' - ', '<br>', $toolTipText)
1345
            . '" data-html="true" data-placement="right"';
1346
        return $toolTipCode;
1347
    }
1348
1349
    /**
1350
     * Returns title-attribute information for ANY record (from a table defined in TCA of course)
1351
     * 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.
1352
     * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page.
1353
     *
1354
     * @param array $row Table row; $row is a row from the table, $table
1355
     * @param string $table Table name
1356
     * @return string
1357
     */
1358
    public static function getRecordIconAltText($row, $table = 'pages')
1359
    {
1360
        if ($table === 'pages') {
1361
            $out = self::titleAttribForPages($row, '', 0);
1362
        } else {
1363
            $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'])) ? $row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] . ' ' : '';
1364
            $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
1365
            // Uid is added
1366
            $out .= 'id=' . $row['uid'];
1367
            if (static::isTableWorkspaceEnabled($table)) {
1368
                switch (VersionState::cast($row['t3ver_state'])) {
1369
                    case new VersionState(VersionState::NEW_PLACEHOLDER):
1370
                        $out .= ' - PLH WSID#' . $row['t3ver_wsid'];
1371
                        break;
1372
                    case new VersionState(VersionState::DELETE_PLACEHOLDER):
1373
                        $out .= ' - Deleted element!';
1374
                        break;
1375
                    case new VersionState(VersionState::MOVE_PLACEHOLDER):
1376
                        $out .= ' - OLD LOCATION (Move Placeholder) WSID#' . $row['t3ver_wsid'];
1377
                        break;
1378
                    case new VersionState(VersionState::MOVE_POINTER):
1379
                        $out .= ' - NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1380
                        break;
1381
                    case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1382
                        $out .= ' - New element!';
1383
                        break;
1384
                }
1385
            }
1386
            // Hidden
1387
            $lang = static::getLanguageService();
1388
            if ($ctrl['disabled']) {
1389
                $out .= $row[$ctrl['disabled']] ? ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') : '';
1390
            }
1391
            if ($ctrl['starttime']) {
1392
                if ($row[$ctrl['starttime']] > $GLOBALS['EXEC_TIME']) {
1393
                    $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') . ')';
1394
                }
1395
            }
1396
            if ($row[$ctrl['endtime']]) {
1397
                $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') . ')';
1398
            }
1399
        }
1400
        return htmlspecialchars($out);
1401
    }
1402
1403
    /**
1404
     * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key
1405
     *
1406
     * @param string $table Table name, present in $GLOBALS['TCA']
1407
     * @param string $col Field name, present in $GLOBALS['TCA']
1408
     * @param string $key items-array value to match
1409
     * @return string Label for item entry
1410
     */
1411
    public static function getLabelFromItemlist($table, $col, $key)
1412
    {
1413
        // Check, if there is an "items" array:
1414
        if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) {
1415
            // Traverse the items-array...
1416
            foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) {
1417
                // ... and return the first found label where the value was equal to $key
1418
                if ((string)$v[1] === (string)$key) {
1419
                    return $v[0];
1420
                }
1421
            }
1422
        }
1423
        return '';
1424
    }
1425
1426
    /**
1427
     * Return the label of a field by additionally checking TsConfig values
1428
     *
1429
     * @param int $pageId Page id
1430
     * @param string $table Table name
1431
     * @param string $column Field Name
1432
     * @param string $key item value
1433
     * @return string Label for item entry
1434
     */
1435
    public static function getLabelFromItemListMerged($pageId, $table, $column, $key)
1436
    {
1437
        $pageTsConfig = static::getPagesTSconfig($pageId);
1438
        $label = '';
1439
        if (isset($pageTsConfig['TCEFORM.'])
1440
            && \is_array($pageTsConfig['TCEFORM.'])
1441
            && \is_array($pageTsConfig['TCEFORM.'][$table . '.'])
1442
            && \is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.'])
1443
        ) {
1444
            if (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'])
1445
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key])
1446
            ) {
1447
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key];
1448
            } elseif (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'])
1449
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key])
1450
            ) {
1451
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key];
1452
            }
1453
        }
1454
        if (empty($label)) {
1455
            $tcaValue = self::getLabelFromItemlist($table, $column, $key);
1456
            if (!empty($tcaValue)) {
1457
                $label = $tcaValue;
1458
            }
1459
        }
1460
        return $label;
1461
    }
1462
1463
    /**
1464
     * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma.
1465
     * NOTE: this does not take itemsProcFunc into account
1466
     *
1467
     * @param string $table Table name, present in TCA
1468
     * @param string $column Field name
1469
     * @param string $keyList Key or comma-separated list of keys.
1470
     * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>)
1471
     * @return string Comma-separated list of localized labels
1472
     */
1473
    public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = [])
1474
    {
1475
        // Check if there is an "items" array
1476
        if (
1477
            !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1478
            || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1479
            || $keyList === ''
1480
        ) {
1481
            return '';
1482
        }
1483
1484
        $keys = GeneralUtility::trimExplode(',', $keyList, true);
1485
        $labels = [];
1486
        // Loop on all selected values
1487
        foreach ($keys as $key) {
1488
            $label = null;
1489
            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...
1490
                // Check if label has been defined or redefined via pageTsConfig
1491
                if (isset($columnTsConfig['addItems.'][$key])) {
1492
                    $label = $columnTsConfig['addItems.'][$key];
1493
                } elseif (isset($columnTsConfig['altLabels.'][$key])) {
1494
                    $label = $columnTsConfig['altLabels.'][$key];
1495
                }
1496
            }
1497
            if ($label === null) {
1498
                // Otherwise lookup the label in TCA items list
1499
                foreach ($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'] as $itemConfiguration) {
1500
                    [$currentLabel, $currentKey] = $itemConfiguration;
1501
                    if ((string)$key === (string)$currentKey) {
1502
                        $label = $currentLabel;
1503
                        break;
1504
                    }
1505
                }
1506
            }
1507
            if ($label !== null) {
1508
                $labels[] = static::getLanguageService()->sL($label);
1509
            }
1510
        }
1511
        return implode(', ', $labels);
1512
    }
1513
1514
    /**
1515
     * Returns the label-value for fieldname $col in table, $table
1516
     * 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>'
1517
     *
1518
     * @param string $table Table name, present in $GLOBALS['TCA']
1519
     * @param string $col Field name
1520
     * @return string or NULL if $col is not found in the TCA table
1521
     */
1522
    public static function getItemLabel($table, $col)
1523
    {
1524
        // Check if column exists
1525
        if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1526
            return $GLOBALS['TCA'][$table]['columns'][$col]['label'];
1527
        }
1528
1529
        return null;
1530
    }
1531
1532
    /**
1533
     * Returns the "title"-value in record, $row, from table, $table
1534
     * The field(s) from which the value is taken is determined by the "ctrl"-entries 'label', 'label_alt' and 'label_alt_force'
1535
     *
1536
     * @param string $table Table name, present in TCA
1537
     * @param array $row Row from table
1538
     * @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
1539
     * @param bool $forceResult If set, the function always returns an output. If no value is found for the title, '[No title]' is returned (localized).
1540
     * @return string
1541
     */
1542
    public static function getRecordTitle($table, $row, $prep = false, $forceResult = true)
1543
    {
1544
        $params = [];
1545
        $recordTitle = '';
1546
        if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table])) {
1547
            // If configured, call userFunc
1548
            if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'])) {
1549
                $params['table'] = $table;
1550
                $params['row'] = $row;
1551
                $params['title'] = '';
1552
                $params['options'] = $GLOBALS['TCA'][$table]['ctrl']['label_userFunc_options'] ?? [];
1553
1554
                // Create NULL-reference
1555
                $null = null;
1556
                GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'], $params, $null);
1557
                $recordTitle = $params['title'];
1558
            } else {
1559
                // No userFunc: Build label
1560
                $recordTitle = self::getProcessedValue(
1561
                    $table,
1562
                    $GLOBALS['TCA'][$table]['ctrl']['label'],
1563
                    $row[$GLOBALS['TCA'][$table]['ctrl']['label']],
1564
                    0,
1565
                    0,
1566
                    false,
1567
                    $row['uid'],
1568
                    $forceResult
1569
                );
1570
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])
1571
                    && (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) || (string)$recordTitle === '')
1572
                ) {
1573
                    $altFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
1574
                    $tA = [];
1575
                    if (!empty($recordTitle)) {
1576
                        $tA[] = $recordTitle;
1577
                    }
1578
                    foreach ($altFields as $fN) {
1579
                        $recordTitle = trim(strip_tags($row[$fN]));
1580
                        if ((string)$recordTitle !== '') {
1581
                            $recordTitle = self::getProcessedValue($table, $fN, $recordTitle, 0, 0, false, $row['uid']);
1582
                            if (!$GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1583
                                break;
1584
                            }
1585
                            $tA[] = $recordTitle;
1586
                        }
1587
                    }
1588
                    if ($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1589
                        $recordTitle = implode(', ', $tA);
1590
                    }
1591
                }
1592
            }
1593
            // If the current result is empty, set it to '[No title]' (localized) and prepare for output if requested
1594
            if ($prep || $forceResult) {
1595
                if ($prep) {
1596
                    $recordTitle = self::getRecordTitlePrep($recordTitle);
1597
                }
1598
                if (trim($recordTitle) === '') {
1599
                    $recordTitle = self::getNoRecordTitle($prep);
1600
                }
1601
            }
1602
        }
1603
1604
        return $recordTitle;
1605
    }
1606
1607
    /**
1608
     * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>,
1609
     * which offers a tooltip with the original title when moving mouse over it.
1610
     *
1611
     * @param string $title The title string to be cropped
1612
     * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
1613
     * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
1614
     */
1615
    public static function getRecordTitlePrep($title, $titleLength = 0)
1616
    {
1617
        // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
1618
        if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
1619
            $titleLength = static::getBackendUserAuthentication()->uc['titleLen'];
1620
        }
1621
        $titleOrig = htmlspecialchars($title);
1622
        $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
1623
        // If title was cropped, offer a tooltip:
1624
        if ($titleOrig != $title) {
1625
            $title = '<span title="' . $titleOrig . '">' . $title . '</span>';
1626
        }
1627
        return $title;
1628
    }
1629
1630
    /**
1631
     * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE.
1632
     *
1633
     * @param bool $prep Wrap result in <em>|</em>
1634
     * @return string Localized [No title] string
1635
     */
1636
    public static function getNoRecordTitle($prep = false)
1637
    {
1638
        $noTitle = '[' .
1639
            htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title'))
1640
            . ']';
1641
        if ($prep) {
1642
            $noTitle = '<em>' . $noTitle . '</em>';
1643
        }
1644
        return $noTitle;
1645
    }
1646
1647
    /**
1648
     * Returns a human readable output of a value from a record
1649
     * 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.
1650
     * $table/$col is tablename and fieldname
1651
     * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant)
1652
     *
1653
     * @param string $table Table name, present in TCA
1654
     * @param string $col Field name, present in TCA
1655
     * @param string $value The value of that field from a selected record
1656
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1657
     * @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")
1658
     * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown.
1659
     * @param int $uid Uid of the current record
1660
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1661
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1662
     * @throws \InvalidArgumentException
1663
     * @return string|null
1664
     */
1665
    public static function getProcessedValue(
1666
        $table,
1667
        $col,
1668
        $value,
1669
        $fixed_lgd_chars = 0,
1670
        $defaultPassthrough = false,
1671
        $noRecordLookup = false,
1672
        $uid = 0,
1673
        $forceResult = true,
1674
        $pid = 0
1675
    ) {
1676
        if ($col === 'uid') {
1677
            // uid is not in TCA-array
1678
            return $value;
1679
        }
1680
        // Check if table and field is configured
1681
        if (!isset($GLOBALS['TCA'][$table]['columns'][$col]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1682
            return null;
1683
        }
1684
        // Depending on the fields configuration, make a meaningful output value.
1685
        $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? [];
1686
        /*****************
1687
         *HOOK: pre-processing the human readable output from a record
1688
         ****************/
1689
        $referenceObject = new \stdClass();
1690
        $referenceObject->table = $table;
1691
        $referenceObject->fieldName = $col;
1692
        $referenceObject->uid = $uid;
1693
        $referenceObject->value = &$value;
1694
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) {
1695
            GeneralUtility::callUserFunction($_funcRef, $theColConf, $referenceObject);
1696
        }
1697
1698
        $l = '';
1699
        $lang = static::getLanguageService();
1700
        switch ((string)($theColConf['type'] ?? '')) {
1701
            case 'radio':
1702
                $l = self::getLabelFromItemlist($table, $col, $value);
1703
                $l = $lang->sL($l);
1704
                break;
1705
            case 'inline':
1706
            case 'select':
1707
                if (!empty($theColConf['MM'])) {
1708
                    if ($uid) {
1709
                        // Display the title of MM related records in lists
1710
                        if ($noRecordLookup) {
1711
                            $MMfields = [];
1712
                            $MMfields[] = $theColConf['foreign_table'] . '.uid';
1713
                        } else {
1714
                            $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1715
                            if (isset($GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'])) {
1716
                                foreach (GeneralUtility::trimExplode(
1717
                                    ',',
1718
                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'],
1719
                                    true
1720
                                ) as $f) {
1721
                                    $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1722
                                }
1723
                            }
1724
                        }
1725
                        /** @var RelationHandler $dbGroup */
1726
                        $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1727
                        $dbGroup->start(
1728
                            $value,
1729
                            $theColConf['foreign_table'],
1730
                            $theColConf['MM'],
1731
                            $uid,
1732
                            $table,
1733
                            $theColConf
1734
                        );
1735
                        $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1736
                        if (is_array($selectUids) && !empty($selectUids)) {
1737
                            $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1738
                            $queryBuilder->getRestrictions()
1739
                                ->removeAll()
1740
                                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1741
1742
                            $result = $queryBuilder
1743
                                ->select('uid', ...$MMfields)
1744
                                ->from($theColConf['foreign_table'])
1745
                                ->where(
1746
                                    $queryBuilder->expr()->in(
1747
                                        'uid',
1748
                                        $queryBuilder->createNamedParameter($selectUids, Connection::PARAM_INT_ARRAY)
1749
                                    )
1750
                                )
1751
                                ->execute();
1752
1753
                            $mmlA = [];
1754
                            while ($MMrow = $result->fetch()) {
1755
                                // Keep sorting of $selectUids
1756
                                $selectedUid = array_search($MMrow['uid'], $selectUids);
1757
                                $mmlA[$selectedUid] = $MMrow['uid'];
1758
                                if (!$noRecordLookup) {
1759
                                    $mmlA[$selectedUid] = static::getRecordTitle(
1760
                                        $theColConf['foreign_table'],
1761
                                        $MMrow,
1762
                                        false,
1763
                                        $forceResult
1764
                                    );
1765
                                }
1766
                            }
1767
1768
                            if (!empty($mmlA)) {
1769
                                ksort($mmlA);
1770
                                $l = implode('; ', $mmlA);
1771
                            } else {
1772
                                $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1773
                            }
1774
                        } else {
1775
                            $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1776
                        }
1777
                    } else {
1778
                        $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1779
                    }
1780
                } else {
1781
                    $columnTsConfig = [];
1782
                    if ($pid) {
1783
                        $pageTsConfig = self::getPagesTSconfig($pid);
1784
                        if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) {
1785
                            $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'];
1786
                        }
1787
                    }
1788
                    $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig);
1789
                    if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) {
1790
                        if ($noRecordLookup) {
1791
                            $l = $value;
1792
                        } else {
1793
                            $rParts = [];
1794
                            if ($uid && isset($theColConf['foreign_field']) && $theColConf['foreign_field'] !== '') {
1795
                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1796
                                $queryBuilder->getRestrictions()
1797
                                    ->removeAll()
1798
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1799
                                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1800
                                $constraints = [
1801
                                    $queryBuilder->expr()->eq(
1802
                                        $theColConf['foreign_field'],
1803
                                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1804
                                    )
1805
                                ];
1806
1807
                                if (!empty($theColConf['foreign_table_field'])) {
1808
                                    $constraints[] = $queryBuilder->expr()->eq(
1809
                                        $theColConf['foreign_table_field'],
1810
                                        $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
1811
                                    );
1812
                                }
1813
1814
                                // Add additional where clause if foreign_match_fields are defined
1815
                                $foreignMatchFields = [];
1816
                                if (is_array($theColConf['foreign_match_fields'])) {
1817
                                    $foreignMatchFields = $theColConf['foreign_match_fields'];
1818
                                }
1819
1820
                                foreach ($foreignMatchFields as $matchField => $matchValue) {
1821
                                    $constraints[] = $queryBuilder->expr()->eq(
1822
                                        $matchField,
1823
                                        $queryBuilder->createNamedParameter($matchValue)
1824
                                    );
1825
                                }
1826
1827
                                $result = $queryBuilder
1828
                                    ->select('*')
1829
                                    ->from($theColConf['foreign_table'])
1830
                                    ->where(...$constraints)
1831
                                    ->execute();
1832
1833
                                while ($record = $result->fetch()) {
1834
                                    $rParts[] = $record['uid'];
1835
                                }
1836
                            }
1837
                            if (empty($rParts)) {
1838
                                $rParts = GeneralUtility::trimExplode(',', $value, true);
1839
                            }
1840
                            $lA = [];
1841
                            foreach ($rParts as $rVal) {
1842
                                $rVal = (int)$rVal;
1843
                                $r = self::getRecordWSOL($theColConf['foreign_table'], $rVal);
1844
                                if (is_array($r)) {
1845
                                    $lA[] = $lang->sL($theColConf['foreign_table_prefix'])
1846
                                        . self::getRecordTitle($theColConf['foreign_table'], $r, false, $forceResult);
1847
                                } else {
1848
                                    $lA[] = $rVal ? '[' . $rVal . '!]' : '';
1849
                                }
1850
                            }
1851
                            $l = implode(', ', $lA);
1852
                        }
1853
                    }
1854
                    if (empty($l) && !empty($value)) {
1855
                        // Use plain database value when label is empty
1856
                        $l = $value;
1857
                    }
1858
                }
1859
                break;
1860
            case 'group':
1861
                // resolve the titles for DB records
1862
                if (isset($theColConf['internal_type']) && $theColConf['internal_type'] === 'db') {
1863
                    if (isset($theColConf['MM']) && $theColConf['MM']) {
1864
                        if ($uid) {
1865
                            // Display the title of MM related records in lists
1866
                            if ($noRecordLookup) {
1867
                                $MMfields = [];
1868
                                $MMfields[] = $theColConf['foreign_table'] . '.uid';
1869
                            } else {
1870
                                $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1871
                                $altLabelFields = explode(
1872
                                    ',',
1873
                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt']
1874
                                );
1875
                                foreach ($altLabelFields as $f) {
1876
                                    $f = trim($f);
1877
                                    if ($f !== '') {
1878
                                        $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1879
                                    }
1880
                                }
1881
                            }
1882
                            /** @var RelationHandler $dbGroup */
1883
                            $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1884
                            $dbGroup->start(
1885
                                $value,
1886
                                $theColConf['foreign_table'],
1887
                                $theColConf['MM'],
1888
                                $uid,
1889
                                $table,
1890
                                $theColConf
1891
                            );
1892
                            $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1893
                            if (!empty($selectUids) && is_array($selectUids)) {
1894
                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1895
                                $queryBuilder->getRestrictions()
1896
                                    ->removeAll()
1897
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1898
1899
                                $result = $queryBuilder
1900
                                    ->select('uid', ...$MMfields)
1901
                                    ->from($theColConf['foreign_table'])
1902
                                    ->where(
1903
                                        $queryBuilder->expr()->in(
1904
                                            'uid',
1905
                                            $queryBuilder->createNamedParameter(
1906
                                                $selectUids,
1907
                                                Connection::PARAM_INT_ARRAY
1908
                                            )
1909
                                        )
1910
                                    )
1911
                                    ->execute();
1912
1913
                                $mmlA = [];
1914
                                while ($MMrow = $result->fetch()) {
1915
                                    // Keep sorting of $selectUids
1916
                                    $selectedUid = array_search($MMrow['uid'], $selectUids);
1917
                                    $mmlA[$selectedUid] = $MMrow['uid'];
1918
                                    if (!$noRecordLookup) {
1919
                                        $mmlA[$selectedUid] = static::getRecordTitle(
1920
                                            $theColConf['foreign_table'],
1921
                                            $MMrow,
1922
                                            false,
1923
                                            $forceResult
1924
                                        );
1925
                                    }
1926
                                }
1927
1928
                                if (!empty($mmlA)) {
1929
                                    ksort($mmlA);
1930
                                    $l = implode('; ', $mmlA);
1931
                                } else {
1932
                                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1933
                                }
1934
                            } else {
1935
                                $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1936
                            }
1937
                        } else {
1938
                            $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1939
                        }
1940
                    } else {
1941
                        $finalValues = [];
1942
                        $relationTableName = $theColConf['allowed'];
1943
                        $explodedValues = GeneralUtility::trimExplode(',', $value, true);
1944
1945
                        foreach ($explodedValues as $explodedValue) {
1946
                            if (MathUtility::canBeInterpretedAsInteger($explodedValue)) {
1947
                                $relationTableNameForField = $relationTableName;
1948
                            } else {
1949
                                [$relationTableNameForField, $explodedValue] = self::splitTable_Uid($explodedValue);
1950
                            }
1951
1952
                            $relationRecord = static::getRecordWSOL($relationTableNameForField, $explodedValue);
0 ignored issues
show
Bug introduced by
$explodedValue of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...tility::getRecordWSOL(). ( Ignorable by Annotation )

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

1952
                            $relationRecord = static::getRecordWSOL($relationTableNameForField, /** @scrutinizer ignore-type */ $explodedValue);
Loading history...
1953
                            $finalValues[] = static::getRecordTitle($relationTableNameForField, $relationRecord);
1954
                        }
1955
                        $l = implode(', ', $finalValues);
1956
                    }
1957
                } else {
1958
                    $l = implode(', ', GeneralUtility::trimExplode(',', $value, true));
1959
                }
1960
                break;
1961
            case 'check':
1962
                if (!is_array($theColConf['items'])) {
1963
                    $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');
1964
                } elseif (count($theColConf['items']) === 1) {
1965
                    reset($theColConf['items']);
1966
                    $invertStateDisplay = current($theColConf['items'])['invertStateDisplay'] ?? false;
1967
                    if ($invertStateDisplay) {
1968
                        $value = !$value;
1969
                    }
1970
                    $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');
1971
                } else {
1972
                    $lA = [];
1973
                    foreach ($theColConf['items'] as $key => $val) {
1974
                        if ($value & 2 ** $key) {
1975
                            $lA[] = $lang->sL($val[0]);
1976
                        }
1977
                    }
1978
                    $l = implode(', ', $lA);
1979
                }
1980
                break;
1981
            case 'input':
1982
                // Hide value 0 for dates, but show it for everything else
1983
                // todo: phpstan states that $value always exists and is not nullable. At the moment, this is a false
1984
                //       positive as null can be passed into this method via $value. As soon as more strict types are
1985
                //       used, this isset check must be replaced with a more appropriate check.
1986
                if (isset($value)) {
1987
                    $dateTimeFormats = QueryHelper::getDateTimeFormats();
1988
1989
                    if (GeneralUtility::inList($theColConf['eval'] ?? '', 'date')) {
1990
                        // Handle native date field
1991
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') {
1992
                            $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value);
1993
                        } else {
1994
                            $value = (int)$value;
1995
                        }
1996
                        if (!empty($value)) {
1997
                            $ageSuffix = '';
1998
                            $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
1999
                            $ageDisplayKey = 'disableAgeDisplay';
2000
2001
                            // generate age suffix as long as not explicitly suppressed
2002
                            if (!isset($dateColumnConfiguration[$ageDisplayKey])
2003
                                // non typesafe comparison on intention
2004
                                || $dateColumnConfiguration[$ageDisplayKey] == false
2005
                            ) {
2006
                                $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '')
2007
                                    . self::calcAge(
2008
                                        abs($GLOBALS['EXEC_TIME'] - $value),
0 ignored issues
show
Bug introduced by
It seems like abs($GLOBALS['EXEC_TIME'] - $value) can also be of type double; however, parameter $seconds of TYPO3\CMS\Backend\Utilit...ckendUtility::calcAge() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2008
                                        /** @scrutinizer ignore-type */ abs($GLOBALS['EXEC_TIME'] - $value),
Loading history...
2009
                                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2010
                                    )
2011
                                    . ')';
2012
                            }
2013
2014
                            $l = self::date($value) . $ageSuffix;
2015
                        }
2016
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'time')) {
2017
                        // Handle native time field
2018
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2019
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
2020
                        } else {
2021
                            $value = (int)$value;
2022
                        }
2023
                        if (!empty($value)) {
2024
                            $l = gmdate('H:i', (int)$value);
2025
                        }
2026
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'timesec')) {
2027
                        // Handle native time field
2028
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2029
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
2030
                        } else {
2031
                            $value = (int)$value;
2032
                        }
2033
                        if (!empty($value)) {
2034
                            $l = gmdate('H:i:s', (int)$value);
2035
                        }
2036
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'datetime')) {
2037
                        // Handle native datetime field
2038
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') {
2039
                            $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value);
2040
                        } else {
2041
                            $value = (int)$value;
2042
                        }
2043
                        if (!empty($value)) {
2044
                            $l = self::datetime($value);
2045
                        }
2046
                    } else {
2047
                        $l = $value;
2048
                    }
2049
                }
2050
                break;
2051
            case 'flex':
2052
                $l = strip_tags($value);
2053
                break;
2054
            default:
2055
                if ($defaultPassthrough) {
2056
                    $l = $value;
2057
                } elseif (isset($theColConf['MM'])) {
2058
                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
2059
                } elseif ($value) {
2060
                    $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200);
2061
                }
2062
        }
2063
        // If this field is a password field, then hide the password by changing it to a random number of asterisk (*)
2064
        if (!empty($theColConf['eval']) && stripos($theColConf['eval'], 'password') !== false) {
2065
            $l = '';
2066
            $randomNumber = random_int(5, 12);
2067
            for ($i = 0; $i < $randomNumber; $i++) {
2068
                $l .= '*';
2069
            }
2070
        }
2071
        /*****************
2072
         *HOOK: post-processing the human readable output from a record
2073
         ****************/
2074
        $null = null;
2075
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) {
2076
            $params = [
2077
                'value' => $l,
2078
                'colConf' => $theColConf
2079
            ];
2080
            $l = GeneralUtility::callUserFunction($_funcRef, $params, $null);
2081
        }
2082
        if ($fixed_lgd_chars) {
2083
            return GeneralUtility::fixed_lgd_cs($l, $fixed_lgd_chars);
2084
        }
2085
        return $l;
2086
    }
2087
2088
    /**
2089
     * 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.
2090
     *
2091
     * @param string $table Table name, present in TCA
2092
     * @param string $fN Field name
2093
     * @param string $fV Field value
2094
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
2095
     * @param int $uid Uid of the current record
2096
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
2097
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
2098
     * @return string
2099
     * @see getProcessedValue()
2100
     */
2101
    public static function getProcessedValueExtra(
2102
        $table,
2103
        $fN,
2104
        $fV,
2105
        $fixed_lgd_chars = 0,
2106
        $uid = 0,
2107
        $forceResult = true,
2108
        $pid = 0
2109
    ) {
2110
        $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, 1, 0, $uid, $forceResult, $pid);
2111
        if (!isset($fVnew)) {
2112
            if (is_array($GLOBALS['TCA'][$table])) {
2113
                if ($fN == $GLOBALS['TCA'][$table]['ctrl']['tstamp'] || $fN == $GLOBALS['TCA'][$table]['ctrl']['crdate']) {
2114
                    $fVnew = self::datetime($fV);
0 ignored issues
show
Bug introduced by
$fV of type string is incompatible with the type integer expected by parameter $value of TYPO3\CMS\Backend\Utilit...kendUtility::datetime(). ( Ignorable by Annotation )

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

2114
                    $fVnew = self::datetime(/** @scrutinizer ignore-type */ $fV);
Loading history...
2115
                } elseif ($fN === 'pid') {
2116
                    // Fetches the path with no regard to the users permissions to select pages.
2117
                    $fVnew = self::getRecordPath($fV, '1=1', 20);
0 ignored issues
show
Bug introduced by
$fV of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...tility::getRecordPath(). ( Ignorable by Annotation )

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

2117
                    $fVnew = self::getRecordPath(/** @scrutinizer ignore-type */ $fV, '1=1', 20);
Loading history...
2118
                } else {
2119
                    $fVnew = $fV;
2120
                }
2121
            }
2122
        }
2123
        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...
2124
    }
2125
2126
    /**
2127
     * Returns fields for a table, $table, which would typically be interesting to select
2128
     * This includes uid, the fields defined for title, icon-field.
2129
     * 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)
2130
     *
2131
     * @param string $table Table name, present in $GLOBALS['TCA']
2132
     * @param string $prefix Table prefix
2133
     * @param array $fields Preset fields (must include prefix if that is used)
2134
     * @return string List of fields.
2135
     * @internal should only be used from within TYPO3 Core
2136
     */
2137
    public static function getCommonSelectFields($table, $prefix = '', $fields = [])
2138
    {
2139
        $fields[] = $prefix . 'uid';
2140
        if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
2141
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
2142
        }
2143
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) {
2144
            $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
2145
            foreach ($secondFields as $fieldN) {
2146
                $fields[] = $prefix . $fieldN;
2147
            }
2148
        }
2149
        if (static::isTableWorkspaceEnabled($table)) {
2150
            $fields[] = $prefix . 't3ver_state';
2151
            $fields[] = $prefix . 't3ver_wsid';
2152
            $fields[] = $prefix . 't3ver_count';
2153
        }
2154
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) {
2155
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
2156
        }
2157
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
2158
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
2159
        }
2160
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
2161
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2162
        }
2163
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) {
2164
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
2165
        }
2166
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) {
2167
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
2168
        }
2169
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
2170
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
2171
        }
2172
        return implode(',', array_unique($fields));
2173
    }
2174
2175
    /*******************************************
2176
     *
2177
     * Backend Modules API functions
2178
     *
2179
     *******************************************/
2180
2181
    /**
2182
     * Returns CSH help text (description), if configured for, as an array (title, description)
2183
     *
2184
     * @param string $table Table name
2185
     * @param string $field Field name
2186
     * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
2187
     * @internal should only be used from within TYPO3 Core
2188
     */
2189
    public static function helpTextArray($table, $field)
2190
    {
2191
        if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
2192
            static::getLanguageService()->loadSingleTableDescription($table);
2193
        }
2194
        $output = [
2195
            'description' => null,
2196
            'title' => null,
2197
            'moreInfo' => false
2198
        ];
2199
        if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
2200
            $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
2201
            // Add alternative title, if defined
2202
            if ($data['alttitle']) {
2203
                $output['title'] = $data['alttitle'];
2204
            }
2205
            // If we have more information to show and access to the cshmanual
2206
            if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax'])
2207
                && static::getBackendUserAuthentication()->check('modules', 'help_CshmanualCshmanual')
2208
            ) {
2209
                $output['moreInfo'] = true;
2210
            }
2211
            // Add description
2212
            if ($data['description']) {
2213
                $output['description'] = $data['description'];
2214
            }
2215
        }
2216
        return $output;
2217
    }
2218
2219
    /**
2220
     * Returns CSH help text
2221
     *
2222
     * @param string $table Table name
2223
     * @param string $field Field name
2224
     * @return string HTML content for help text
2225
     * @see cshItem()
2226
     * @internal should only be used from within TYPO3 Core
2227
     */
2228
    public static function helpText($table, $field)
2229
    {
2230
        $helpTextArray = self::helpTextArray($table, $field);
2231
        $output = '';
2232
        $arrow = '';
2233
        // Put header before the rest of the text
2234
        if ($helpTextArray['title'] !== null) {
2235
            $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
2236
        }
2237
        // Add see also arrow if we have more info
2238
        if ($helpTextArray['moreInfo']) {
2239
            /** @var IconFactory $iconFactory */
2240
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2241
            $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
2242
        }
2243
        // Wrap description and arrow in p tag
2244
        if ($helpTextArray['description'] !== null || $arrow) {
2245
            $output .= '<p class="help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>';
2246
        }
2247
        return $output;
2248
    }
2249
2250
    /**
2251
     * API function that wraps the text / html in help text, so if a user hovers over it
2252
     * the help text will show up
2253
     *
2254
     * @param string $table The table name for which the help should be shown
2255
     * @param string $field The field name for which the help should be shown
2256
     * @param string $text The text which should be wrapped with the help text
2257
     * @param array $overloadHelpText Array with text to overload help text
2258
     * @return string the HTML code ready to render
2259
     * @internal should only be used from within TYPO3 Core
2260
     */
2261
    public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
2262
    {
2263
        // Initialize some variables
2264
        $helpText = '';
2265
        $abbrClassAdd = '';
2266
        $hasHelpTextOverload = !empty($overloadHelpText);
2267
        // Get the help text that should be shown on hover
2268
        if (!$hasHelpTextOverload) {
2269
            $helpText = self::helpText($table, $field);
2270
        }
2271
        // If there's a help text or some overload information, proceed with preparing an output
2272
        if (!empty($helpText) || $hasHelpTextOverload) {
2273
            // If no text was given, just use the regular help icon
2274
            if ($text == '') {
2275
                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2276
                $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2277
                $abbrClassAdd = ' help-teaser-icon';
2278
            }
2279
            $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2280
            $wrappedText = '<span class="help-link" href="#" data-table="' . $table . '" data-field="' . $field . '"';
2281
            // The overload array may provide a title and a description
2282
            // If either one is defined, add them to the "data" attributes
2283
            if ($hasHelpTextOverload) {
2284
                if (isset($overloadHelpText['title'])) {
2285
                    $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2286
                }
2287
                if (isset($overloadHelpText['description'])) {
2288
                    $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2289
                }
2290
            }
2291
            $wrappedText .= '>' . $text . '</span>';
2292
            return $wrappedText;
2293
        }
2294
        return $text;
2295
    }
2296
2297
    /**
2298
     * API for getting CSH icons/text for use in backend modules.
2299
     * TCA_DESCR will be loaded if it isn't already
2300
     *
2301
     * @param string $table Table name ('_MOD_'+module name)
2302
     * @param string $field Field name (CSH locallang main key)
2303
     * @param string $_ (unused)
2304
     * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2305
     * @return string HTML content for help text
2306
     */
2307
    public static function cshItem($table, $field, $_ = '', $wrap = '')
2308
    {
2309
        static::getLanguageService()->loadSingleTableDescription($table);
2310
        if (is_array($GLOBALS['TCA_DESCR'][$table])
2311
            && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])
2312
        ) {
2313
            // Creating short description
2314
            $output = self::wrapInHelp($table, $field);
2315
            if ($output && $wrap) {
2316
                $wrParts = explode('|', $wrap);
2317
                $output = $wrParts[0] . $output . $wrParts[1];
2318
            }
2319
            return $output;
2320
        }
2321
        return '';
2322
    }
2323
2324
    /**
2325
     * Returns a JavaScript string (for an onClick handler) which will load the EditDocumentController script that shows the form for editing of the record(s) you have send as params.
2326
     * REMEMBER to always htmlspecialchar() content in href-properties to ampersands get converted to entities (XHTML requirement and XSS precaution)
2327
     *
2328
     * @param string $params Parameters sent along to EditDocumentController. This requires a much more details description which you must seek in Inside TYPO3s documentation of the FormEngine API. And example could be '&edit[pages][123] = edit' which will show edit form for page record 123.
2329
     * @param string $_ (unused)
2330
     * @param string $requestUri An optional returnUrl you can set - automatically set to REQUEST_URI.
2331
     *
2332
     * @return string
2333
     * @deprecated will be removed in TYPO3 v11.
2334
     */
2335
    public static function editOnClick($params, $_ = '', $requestUri = '')
2336
    {
2337
        trigger_error(__METHOD__ . ' has been marked as deprecated and will be removed in TYPO3 v11. Consider using regular links and use the UriBuilder API instead.', E_USER_DEPRECATED);
2338
        if ($requestUri == -1) {
2339
            $returnUrl = 'T3_THIS_LOCATION';
2340
        } else {
2341
            $returnUrl = GeneralUtility::quoteJSvalue(rawurlencode($requestUri ?: GeneralUtility::getIndpEnv('REQUEST_URI')));
2342
        }
2343
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2344
        return 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('record_edit') . $params . '&returnUrl=') . '+' . $returnUrl . '; return false;';
2345
    }
2346
2347
    /**
2348
     * Returns a JavaScript string for viewing the page id, $id
2349
     * It will re-use any window already open.
2350
     *
2351
     * @param int $pageUid Page UID
2352
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2353
     * @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)
2354
     * @param string $anchorSection Optional anchor to the URL
2355
     * @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!
2356
     * @param string $additionalGetVars Additional GET variables.
2357
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2358
     * @return string
2359
     */
2360
    public static function viewOnClick(
2361
        $pageUid,
2362
        $backPath = '',
2363
        $rootLine = null,
2364
        $anchorSection = '',
2365
        $alternativeUrl = '',
2366
        $additionalGetVars = '',
2367
        $switchFocus = true
2368
    ) {
2369
        try {
2370
            $previewUrl = self::getPreviewUrl(
2371
                $pageUid,
2372
                $backPath,
2373
                $rootLine,
2374
                $anchorSection,
2375
                $alternativeUrl,
2376
                $additionalGetVars,
2377
                $switchFocus
2378
            );
2379
        } catch (UnableToLinkToPageException $e) {
2380
            return '';
2381
        }
2382
2383
        $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');'
2384
            . ($switchFocus ? 'previewWin.focus();' : '') . LF
2385
            . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };';
2386
2387
        return $onclickCode;
2388
    }
2389
2390
    /**
2391
     * Returns the preview url
2392
     *
2393
     * It will detect the correct domain name if needed and provide the link with the right back path.
2394
     *
2395
     * @param int $pageUid Page UID
2396
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2397
     * @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)
2398
     * @param string $anchorSection Optional anchor to the URL
2399
     * @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!
2400
     * @param string $additionalGetVars Additional GET variables.
2401
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2402
     * @return string
2403
     */
2404
    public static function getPreviewUrl(
2405
        $pageUid,
2406
        $backPath = '',
2407
        $rootLine = null,
2408
        $anchorSection = '',
2409
        $alternativeUrl = '',
2410
        $additionalGetVars = '',
2411
        &$switchFocus = true
2412
    ): string {
2413
        $viewScript = '/index.php?id=';
2414
        if ($alternativeUrl) {
2415
            $viewScript = $alternativeUrl;
2416
        }
2417
2418
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2419
            $hookObj = GeneralUtility::makeInstance($className);
2420
            if (method_exists($hookObj, 'preProcess')) {
2421
                $hookObj->preProcess(
2422
                    $pageUid,
2423
                    $backPath,
2424
                    $rootLine,
2425
                    $anchorSection,
2426
                    $viewScript,
2427
                    $additionalGetVars,
2428
                    $switchFocus
2429
                );
2430
            }
2431
        }
2432
2433
        // If there is an alternative URL or the URL has been modified by a hook, use that one.
2434
        if ($alternativeUrl || $viewScript !== '/index.php?id=') {
2435
            $previewUrl = $viewScript;
2436
        } else {
2437
            $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2438
            $pageInfo = self::readPageAccess($pageUid, $permissionClause);
2439
            // prepare custom context for link generation (to allow for example time based previews)
2440
            $context = clone GeneralUtility::makeInstance(Context::class);
2441
            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context);
2442
2443
            // Build the URL with a site as prefix, if configured
2444
            $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
2445
            // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is
2446
            $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2447
            try {
2448
                $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine);
2449
            } catch (SiteNotFoundException $e) {
2450
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794919);
2451
            }
2452
            // Create a multi-dimensional array out of the additional get vars
2453
            $additionalQueryParams = [];
2454
            parse_str($additionalGetVars, $additionalQueryParams);
2455
            if (isset($additionalQueryParams['L'])) {
2456
                $additionalQueryParams['_language'] = $additionalQueryParams['_language'] ?? $additionalQueryParams['L'];
2457
                unset($additionalQueryParams['L']);
2458
            }
2459
            try {
2460
                $previewUrl = (string)$site->getRouter($context)->generateUri(
2461
                    $pageUid,
2462
                    $additionalQueryParams,
2463
                    $anchorSection,
2464
                    RouterInterface::ABSOLUTE_URL
2465
                );
2466
            } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
2467
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794914);
2468
            }
2469
        }
2470
2471
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2472
            $hookObj = GeneralUtility::makeInstance($className);
2473
            if (method_exists($hookObj, 'postProcess')) {
2474
                $previewUrl = $hookObj->postProcess(
2475
                    $previewUrl,
2476
                    $pageUid,
2477
                    $rootLine,
2478
                    $anchorSection,
2479
                    $viewScript,
2480
                    $additionalGetVars,
2481
                    $switchFocus
2482
                );
2483
            }
2484
        }
2485
2486
        return $previewUrl;
2487
    }
2488
2489
    /**
2490
     * Makes click menu link (context sensitive menu)
2491
     *
2492
     * Returns $str wrapped in a link which will activate the context sensitive
2493
     * menu for the record ($table/$uid) or file ($table = file)
2494
     * The link will load the top frame with the parameter "&item" which is the table, uid
2495
     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2496
     *
2497
     * @param string $content String to be wrapped in link, typ. image tag.
2498
     * @param string $table Table name/File path. If the icon is for a database
2499
     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2500
     * the absolute filepath
2501
     * @param int|string $uid If icon is for database record this is the UID for the
2502
     * record from $table or identifier for sys_file record
2503
     * @param string $context Set tree if menu is called from tree view
2504
     * @param string $_addParams NOT IN USE
2505
     * @param string $_enDisItems NOT IN USE
2506
     * @param bool $returnTagParameters If set, will return only the onclick
2507
     * JavaScript, not the whole link.
2508
     *
2509
     * @return string The link wrapped input string.
2510
     */
2511
    public static function wrapClickMenuOnIcon(
2512
        $content,
2513
        $table,
2514
        $uid = 0,
2515
        $context = '',
2516
        $_addParams = '',
2517
        $_enDisItems = '',
2518
        $returnTagParameters = false
2519
    ) {
2520
        $tagParameters = [
2521
            'class' => 't3js-contextmenutrigger',
2522
            'data-table' => $table,
2523
            'data-uid' => $uid,
2524
            'data-context' => $context
2525
        ];
2526
2527
        if ($returnTagParameters) {
2528
            return $tagParameters;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $tagParameters returns the type array<string,integer|string> which is incompatible with the documented return type string.
Loading history...
2529
        }
2530
        return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2531
    }
2532
2533
    /**
2534
     * Returns a URL with a command to TYPO3 Datahandler
2535
     *
2536
     * @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
2537
     * @param string|int $redirectUrl Redirect URL, default is to use GeneralUtility::getIndpEnv('REQUEST_URI'), -1 means to generate an URL for JavaScript using T3_THIS_LOCATION
2538
     * @return string
2539
     */
2540
    public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2541
    {
2542
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2543
        $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2544
        if ((int)$redirectUrl === -1) {
2545
            trigger_error('Generating URLs to DataHandler for JavaScript click handlers is deprecated. Consider using the href attribute instead.', E_USER_DEPRECATED);
2546
            $url = GeneralUtility::quoteJSvalue($url) . '+T3_THIS_LOCATION';
2547
        } else {
2548
            $url .= rawurlencode($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI'));
2549
        }
2550
        return $url;
2551
    }
2552
2553
    /**
2554
     * Builds the frontend view domain for a given page ID with a given root
2555
     * line.
2556
     *
2557
     * @param int $pageId The page ID to use, must be > 0
2558
     * @param array|null $rootLine The root line structure to use
2559
     * @return string The full domain including the protocol http:// or https://, but without the trailing '/'
2560
     * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0. Use PageRouter instead.
2561
     */
2562
    public static function getViewDomain($pageId, $rootLine = null)
2563
    {
2564
        trigger_error('BackendUtility::getViewDomain() will be removed in TYPO3 v11.0. Use a Site and its PageRouter to link to a page directly', E_USER_DEPRECATED);
2565
        $domain = rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), '/');
2566
        if (!is_array($rootLine)) {
2567
            $rootLine = self::BEgetRootLine($pageId);
2568
        }
2569
        // Checks alternate domains
2570
        if (!empty($rootLine)) {
2571
            try {
2572
                $site = GeneralUtility::makeInstance(SiteFinder::class)
2573
                    ->getSiteByPageId((int)$pageId, $rootLine);
2574
                $uri = $site->getBase();
2575
            } catch (SiteNotFoundException $e) {
2576
                // Just use the current domain
2577
                $uri = new Uri($domain);
2578
                // Append port number if lockSSLPort is not the standard port 443
2579
                $portNumber = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort'];
2580
                if ($portNumber > 0 && $portNumber !== 443 && $portNumber < 65536 && $uri->getScheme() === 'https') {
2581
                    $uri = $uri->withPort((int)$portNumber);
2582
                }
2583
            }
2584
            return (string)$uri;
2585
        }
2586
        return $domain;
2587
    }
2588
2589
    /**
2590
     * Returns a selector box "function menu" for a module
2591
     * See Inside TYPO3 for details about how to use / make Function menus
2592
     *
2593
     * @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=...
2594
     * @param string $elementName The form elements name, probably something like "SET[...]
2595
     * @param string $currentValue The value to be selected currently.
2596
     * @param array $menuItems An array with the menu items for the selector box
2597
     * @param string $script The script to send the &id to, if empty it's automatically found
2598
     * @param string $addParams Additional parameters to pass to the script.
2599
     * @return string HTML code for selector box
2600
     */
2601
    public static function getFuncMenu(
2602
        $mainParams,
2603
        $elementName,
2604
        $currentValue,
2605
        $menuItems,
2606
        $script = '',
2607
        $addParams = ''
2608
    ) {
2609
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2610
            return '';
2611
        }
2612
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2613
        $options = [];
2614
        foreach ($menuItems as $value => $label) {
2615
            $options[] = '<option value="'
2616
                . htmlspecialchars($value) . '"'
2617
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2618
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2619
        }
2620
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2621
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2622
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2623
        if (!empty($options)) {
2624
            // @todo Should we add requireJsModule again (should be loaded in most/all cases)
2625
            // loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
2626
            $attributes = GeneralUtility::implodeAttributes([
2627
                'name' => $elementName,
2628
                'class' => 'form-control',
2629
                'data-menu-identifier' => $dataMenuIdentifier,
2630
                'data-global-event' => 'change',
2631
                'data-action-navigate' => '$data=~s/$value/',
2632
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2633
            ], true);
2634
            return sprintf(
2635
                '<select %s>%s</select>select>',
2636
                $attributes,
2637
                implode('', $options)
2638
            );
2639
        }
2640
        return '';
2641
    }
2642
2643
    /**
2644
     * Returns a selector box to switch the view
2645
     * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose.
2646
     * Mingling with getFuncMenu would harm the docHeader Menu.
2647
     *
2648
     * @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=...
2649
     * @param string $elementName The form elements name, probably something like "SET[...]
2650
     * @param string $currentValue The value to be selected currently.
2651
     * @param array $menuItems An array with the menu items for the selector box
2652
     * @param string $script The script to send the &id to, if empty it's automatically found
2653
     * @param string $addParams Additional parameters to pass to the script.
2654
     * @return string HTML code for selector box
2655
     */
2656
    public static function getDropdownMenu(
2657
        $mainParams,
2658
        $elementName,
2659
        $currentValue,
2660
        $menuItems,
2661
        $script = '',
2662
        $addParams = ''
2663
    ) {
2664
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2665
            return '';
2666
        }
2667
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2668
        $options = [];
2669
        foreach ($menuItems as $value => $label) {
2670
            $options[] = '<option value="'
2671
                . htmlspecialchars($value) . '"'
2672
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2673
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2674
        }
2675
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2676
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2677
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2678
        if (!empty($options)) {
2679
            // @todo Should we add requireJsModule again (should be loaded in most/all cases)
2680
            // loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
2681
            $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+this.options[this.selectedIndex].value;';
0 ignored issues
show
Unused Code introduced by
The assignment to $onChange is dead and can be removed.
Loading history...
2682
            $attributes = GeneralUtility::implodeAttributes([
2683
                'name' => $elementName,
2684
                'data-menu-identifier' => $dataMenuIdentifier,
2685
                'data-global-event' => 'change',
2686
                'data-action-navigate' => '$data=~s/$value/',
2687
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2688
            ], true);
2689
            return '
2690
			<div class="form-group">
2691
				<!-- Function Menu of module -->
2692
				<select class="form-control input-sm" ' . $attributes . '>
2693
					' . implode(LF, $options) . '
2694
				</select>
2695
			</div>
2696
						';
2697
        }
2698
        return '';
2699
    }
2700
2701
    /**
2702
     * Checkbox function menu.
2703
     * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox.
2704
     *
2705
     * @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=...
2706
     * @param string $elementName The form elements name, probably something like "SET[...]
2707
     * @param string $currentValue The value to be selected currently.
2708
     * @param string $script The script to send the &id to, if empty it's automatically found
2709
     * @param string $addParams Additional parameters to pass to the script.
2710
     * @param string $tagParams Additional attributes for the checkbox input tag
2711
     * @return string HTML code for checkbox
2712
     * @see getFuncMenu()
2713
     */
2714
    public static function getFuncCheck(
2715
        $mainParams,
2716
        $elementName,
2717
        $currentValue,
2718
        $script = '',
2719
        $addParams = '',
2720
        $tagParams = ''
2721
    ) {
2722
        // @todo Should we add requireJsModule again (should be loaded in most/all cases)
2723
        // loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
2724
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2725
        $attributes = GeneralUtility::implodeAttributes([
2726
            'type' => 'checkbox',
2727
            'class' => 'checkbox',
2728
            'name' => $elementName,
2729
            'value' => 1,
2730
            'data-global-event' => 'change',
2731
            'data-action-navigate' => '$data=~s/$value/',
2732
            'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName),
2733
        ], true);
2734
        return
2735
            '<input ' . $attributes .
2736
            ($currentValue ? ' checked="checked"' : '') .
2737
            ($tagParams ? ' ' . $tagParams : '') .
2738
            ' />';
2739
    }
2740
2741
    /**
2742
     * Input field function menu
2743
     * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange"
2744
     *
2745
     * @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=...
2746
     * @param string $elementName The form elements name, probably something like "SET[...]
2747
     * @param string $currentValue The value to be selected currently.
2748
     * @param int $size Relative size of input field, max is 48
2749
     * @param string $script The script to send the &id to, if empty it's automatically found
2750
     * @param string $addParams Additional parameters to pass to the script.
2751
     * @return string HTML code for input text field.
2752
     * @see getFuncMenu()
2753
     */
2754
    public static function getFuncInput(
2755
        $mainParams,
2756
        $elementName,
2757
        $currentValue,
2758
        $size = 10,
2759
        $script = '',
2760
        $addParams = ''
2761
    ) {
2762
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2763
        $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value);';
2764
        return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />';
2765
    }
2766
2767
    /**
2768
     * Builds the URL to the current script with given arguments
2769
     *
2770
     * @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=...
2771
     * @param string $addParams Additional parameters to pass to the script.
2772
     * @param string $script The script to send the &id to, if empty it's automatically found
2773
     * @return string The complete script URL
2774
     */
2775
    protected static function buildScriptUrl($mainParams, $addParams, $script = '')
2776
    {
2777
        if (!is_array($mainParams)) {
2778
            $mainParams = ['id' => $mainParams];
2779
        }
2780
        if (!$script) {
2781
            $script = PathUtility::basename(Environment::getCurrentScript());
2782
        }
2783
2784
        if ($routePath = GeneralUtility::_GP('route')) {
2785
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2786
            $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
2787
            $scriptUrl .= $addParams;
2788
        } else {
2789
            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
2790
        }
2791
2792
        return $scriptUrl;
2793
    }
2794
2795
    /**
2796
     * Call to update the page tree frame (or something else..?) after
2797
     * use 'updatePageTree' as a first parameter will set the page tree to be updated.
2798
     *
2799
     * @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.
2800
     * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
2801
     * @see BackendUtility::getUpdateSignalCode()
2802
     */
2803
    public static function setUpdateSignal($set = '', $params = '')
2804
    {
2805
        $beUser = static::getBackendUserAuthentication();
2806
        $modData = $beUser->getModuleData(
2807
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2808
            'ses'
2809
        );
2810
        if ($set) {
2811
            $modData[$set] = [
2812
                'set' => $set,
2813
                'parameter' => $params
2814
            ];
2815
        } else {
2816
            // clear the module data
2817
            $modData = [];
2818
        }
2819
        $beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2820
    }
2821
2822
    /**
2823
     * Call to update the page tree frame (or something else..?) if this is set by the function
2824
     * setUpdateSignal(). It will return some JavaScript that does the update
2825
     *
2826
     * @return string HTML javascript code
2827
     * @see BackendUtility::setUpdateSignal()
2828
     */
2829
    public static function getUpdateSignalCode()
2830
    {
2831
        $signals = [];
2832
        $modData = static::getBackendUserAuthentication()->getModuleData(
2833
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2834
            'ses'
2835
        );
2836
        if (empty($modData)) {
2837
            return '';
2838
        }
2839
        // Hook: Allows to let TYPO3 execute your JS code
2840
        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
2841
        // Loop through all setUpdateSignals and get the JS code
2842
        foreach ($modData as $set => $val) {
2843
            if (isset($updateSignals[$set])) {
2844
                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => ''];
2845
                $ref = null;
2846
                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
2847
                $signals[] = $params['JScode'];
2848
            } else {
2849
                switch ($set) {
2850
                    case 'updatePageTree':
2851
                        $signals[] = '
2852
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) {
2853
									top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
2854
								}
2855
							';
2856
                        break;
2857
                    case 'updateFolderTree':
2858
                        $signals[] = '
2859
								if (top && top.nav_frame && top.nav_frame.location) {
2860
									top.nav_frame.location.reload(true);
2861
								}';
2862
                        break;
2863
                    case 'updateModuleMenu':
2864
                        $signals[] = '
2865
								if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) {
2866
									top.TYPO3.ModuleMenu.App.refreshMenu();
2867
								}';
2868
                        break;
2869
                    case 'updateTopbar':
2870
                        $signals[] = '
2871
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) {
2872
									top.TYPO3.Backend.Topbar.refresh();
2873
								}';
2874
                        break;
2875
                }
2876
            }
2877
        }
2878
        $content = implode(LF, $signals);
2879
        // For backwards compatibility, should be replaced
2880
        self::setUpdateSignal();
2881
        return $content;
2882
    }
2883
2884
    /**
2885
     * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
2886
     * This is kind of session variable management framework for the backend users.
2887
     * 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
2888
     * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules.
2889
     *
2890
     * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus.
2891
     * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus.
2892
     * @param string $modName modName is the name of this module. Used to get the correct module data.
2893
     * @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.
2894
     * @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.
2895
     * @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)
2896
     * @throws \RuntimeException
2897
     * @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
2898
     */
2899
    public static function getModuleData(
2900
        $MOD_MENU,
2901
        $CHANGED_SETTINGS,
2902
        $modName,
2903
        $type = '',
2904
        $dontValidateList = '',
2905
        $setDefaultList = ''
2906
    ) {
2907
        if ($modName && is_string($modName)) {
2908
            // Getting stored user-data from this module:
2909
            $beUser = static::getBackendUserAuthentication();
2910
            $settings = $beUser->getModuleData($modName, $type);
2911
            $changed = 0;
2912
            if (!is_array($settings)) {
2913
                $changed = 1;
2914
                $settings = [];
2915
            }
2916
            if (is_array($MOD_MENU)) {
0 ignored issues
show
introduced by
The condition is_array($MOD_MENU) is always true.
Loading history...
2917
                foreach ($MOD_MENU as $key => $var) {
2918
                    // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array.
2919
                    if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) {
2920
                        if (is_array($CHANGED_SETTINGS[$key])) {
2921
                            $serializedSettings = serialize($CHANGED_SETTINGS[$key]);
2922
                            if ((string)$settings[$key] !== $serializedSettings) {
2923
                                $settings[$key] = $serializedSettings;
2924
                                $changed = 1;
2925
                            }
2926
                        } else {
2927
                            if ((string)$settings[$key] !== (string)$CHANGED_SETTINGS[$key]) {
2928
                                $settings[$key] = $CHANGED_SETTINGS[$key];
2929
                                $changed = 1;
2930
                            }
2931
                        }
2932
                    }
2933
                    // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted
2934
                    if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) {
2935
                        // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted.
2936
                        if (is_array($settings[$key]) || !isset($MOD_MENU[$key][$settings[$key]])) {
2937
                            $settings[$key] = (string)key($var);
2938
                            $changed = 1;
2939
                        }
2940
                    }
2941
                    // Sets default values (only strings/checkboxes, not menus)
2942
                    if ($setDefaultList && !is_array($var)) {
2943
                        if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) {
2944
                            $settings[$key] = (string)$var;
2945
                        }
2946
                    }
2947
                }
2948
            } else {
2949
                throw new \RuntimeException('No menu', 1568119229);
2950
            }
2951
            if ($changed) {
2952
                $beUser->pushModuleData($modName, $settings);
2953
            }
2954
            return $settings;
2955
        }
2956
        throw new \RuntimeException('Wrong module name "' . $modName . '"', 1568119221);
2957
    }
2958
2959
    /*******************************************
2960
     *
2961
     * Core
2962
     *
2963
     *******************************************/
2964
    /**
2965
     * Unlock or Lock a record from $table with $uid
2966
     * If $table and $uid is not set, then all locking for the current BE_USER is removed!
2967
     *
2968
     * @param string $table Table name
2969
     * @param int $uid Record uid
2970
     * @param int $pid Record pid
2971
     * @internal
2972
     */
2973
    public static function lockRecords($table = '', $uid = 0, $pid = 0)
2974
    {
2975
        $beUser = static::getBackendUserAuthentication();
2976
        if (isset($beUser->user['uid'])) {
2977
            $userId = (int)$beUser->user['uid'];
2978
            if ($table && $uid) {
2979
                $fieldsValues = [
2980
                    'userid' => $userId,
2981
                    'feuserid' => 0,
2982
                    'tstamp' => $GLOBALS['EXEC_TIME'],
2983
                    'record_table' => $table,
2984
                    'record_uid' => $uid,
2985
                    'username' => $beUser->user['username'],
2986
                    'record_pid' => $pid
2987
                ];
2988
                GeneralUtility::makeInstance(ConnectionPool::class)
2989
                    ->getConnectionForTable('sys_lockedrecords')
2990
                    ->insert(
2991
                        'sys_lockedrecords',
2992
                        $fieldsValues
2993
                    );
2994
            } else {
2995
                GeneralUtility::makeInstance(ConnectionPool::class)
2996
                    ->getConnectionForTable('sys_lockedrecords')
2997
                    ->delete(
2998
                        'sys_lockedrecords',
2999
                        ['userid' => (int)$userId]
3000
                    );
3001
            }
3002
        }
3003
    }
3004
3005
    /**
3006
     * Returns information about whether the record from table, $table, with uid, $uid is currently locked
3007
     * (edited by another user - which should issue a warning).
3008
     * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts
3009
     * are activated - which means that a user CAN have a record "open" without having it locked.
3010
     * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient.
3011
     *
3012
     * @param string $table Table name
3013
     * @param int $uid Record uid
3014
     * @return array|bool
3015
     * @internal
3016
     */
3017
    public static function isRecordLocked($table, $uid)
3018
    {
3019
        $runtimeCache = self::getRuntimeCache();
3020
        $cacheId = 'backend-recordLocked';
3021
        $recordLockedCache = $runtimeCache->get($cacheId);
3022
        if ($recordLockedCache !== false) {
3023
            $lockedRecords = $recordLockedCache;
3024
        } else {
3025
            $lockedRecords = [];
3026
3027
            $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords');
3028
            $result = $queryBuilder
3029
                ->select('*')
3030
                ->from('sys_lockedrecords')
3031
                ->where(
3032
                    $queryBuilder->expr()->neq(
3033
                        'sys_lockedrecords.userid',
3034
                        $queryBuilder->createNamedParameter(
3035
                            static::getBackendUserAuthentication()->user['uid'],
3036
                            \PDO::PARAM_INT
3037
                        )
3038
                    ),
3039
                    $queryBuilder->expr()->gt(
3040
                        'sys_lockedrecords.tstamp',
3041
                        $queryBuilder->createNamedParameter(
3042
                            $GLOBALS['EXEC_TIME'] - 2 * 3600,
3043
                            \PDO::PARAM_INT
3044
                        )
3045
                    )
3046
                )
3047
                ->execute();
3048
3049
            $lang = static::getLanguageService();
3050
            while ($row = $result->fetch()) {
3051
                // Get the type of the user that locked this record:
3052
                if ($row['userid']) {
3053
                    $userTypeLabel = 'beUser';
3054
                } elseif ($row['feuserid']) {
3055
                    $userTypeLabel = 'feUser';
3056
                } else {
3057
                    $userTypeLabel = 'user';
3058
                }
3059
                $userType = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel);
3060
                // Get the username (if available):
3061
                if ($row['username']) {
3062
                    $userName = $row['username'];
3063
                } else {
3064
                    $userName = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.unknownUser');
3065
                }
3066
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row;
3067
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf(
3068
                    $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'),
3069
                    $userType,
3070
                    $userName,
3071
                    self::calcAge(
3072
                        $GLOBALS['EXEC_TIME'] - $row['tstamp'],
3073
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
3074
                    )
3075
                );
3076
                if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) {
3077
                    $lockedRecords['pages:' . $row['record_pid']]['msg'] = sprintf(
3078
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'),
3079
                        $userType,
3080
                        $userName,
3081
                        self::calcAge(
3082
                            $GLOBALS['EXEC_TIME'] - $row['tstamp'],
3083
                            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
3084
                        )
3085
                    );
3086
                }
3087
            }
3088
            $runtimeCache->set($cacheId, $lockedRecords);
3089
        }
3090
3091
        return $lockedRecords[$table . ':' . $uid] ?? false;
3092
    }
3093
3094
    /**
3095
     * Returns TSConfig for the TCEFORM object in Page TSconfig.
3096
     * Used in TCEFORMs
3097
     *
3098
     * @param string $table Table name present in TCA
3099
     * @param array $row Row from table
3100
     * @return array
3101
     */
3102
    public static function getTCEFORM_TSconfig($table, $row)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::getTCEFORM_TSconfig" is not in camel caps format
Loading history...
3103
    {
3104
        self::fixVersioningPid($table, $row);
3105
        $res = [];
3106
        // Get main config for the table
3107
        [$TScID, $cPid] = self::getTSCpid($table, $row['uid'], $row['pid']);
3108
        if ($TScID >= 0) {
3109
            $tsConfig = static::getPagesTSconfig($TScID)['TCEFORM.'][$table . '.'] ?? [];
3110
            $typeVal = self::getTCAtypeValue($table, $row);
3111
            foreach ($tsConfig as $key => $val) {
3112
                if (is_array($val)) {
3113
                    $fieldN = substr($key, 0, -1);
3114
                    $res[$fieldN] = $val;
3115
                    unset($res[$fieldN]['types.']);
3116
                    if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'])) {
3117
                        ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']);
3118
                    }
3119
                }
3120
            }
3121
        }
3122
        $res['_CURRENT_PID'] = $cPid;
3123
        $res['_THIS_UID'] = $row['uid'];
3124
        // So the row will be passed to foreign_table_where_query()
3125
        $res['_THIS_ROW'] = $row;
3126
        return $res;
3127
    }
3128
3129
    /**
3130
     * Find the real PID of the record (with $uid from $table).
3131
     * 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).
3132
     * NOTICE: Make sure that the input PID is never negative because the record was an offline version!
3133
     * Therefore, you should always use BackendUtility::fixVersioningPid($table,$row); on the data you input before calling this function!
3134
     *
3135
     * @param string $table Table name
3136
     * @param int $uid Record uid
3137
     * @param int $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return
3138
     * @return int
3139
     * @internal
3140
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord()
3141
     * @see \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid()
3142
     */
3143
    public static function getTSconfig_pidValue($table, $uid, $pid)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::getTSconfig_pidValue" is not in camel caps format
Loading history...
3144
    {
3145
        // If pid is an integer this takes precedence in our lookup.
3146
        if (MathUtility::canBeInterpretedAsInteger($pid)) {
3147
            $thePidValue = (int)$pid;
3148
            // If ref to another record, look that record up.
3149
            if ($thePidValue < 0) {
3150
                $pidRec = self::getRecord($table, abs($thePidValue), 'pid');
0 ignored issues
show
Bug introduced by
It seems like abs($thePidValue) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

3150
                $pidRec = self::getRecord($table, /** @scrutinizer ignore-type */ abs($thePidValue), 'pid');
Loading history...
3151
                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
3152
            }
3153
        } else {
3154
            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
3155
            $rr = self::getRecord($table, $uid);
3156
            $thePidValue = null;
3157
            if (is_array($rr)) {
3158
                // First check if the pid is -1 which means it is a workspaced element. Get the "real" record:
3159
                if ($rr['pid'] == '-1') {
3160
                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
3161
                    if (is_array($rr)) {
3162
                        $thePidValue = $rr['pid'];
3163
                    }
3164
                } else {
3165
                    // Returning the "pid" of the record
3166
                    $thePidValue = $rr['pid'];
3167
                }
3168
            }
3169
            if (!$thePidValue) {
3170
                // Returns -1 if the record with this pid was not found.
3171
                $thePidValue = -1;
3172
            }
3173
        }
3174
        return $thePidValue;
3175
    }
3176
3177
    /**
3178
     * Return the real pid of a record and caches the result.
3179
     * The non-cached method needs database queries to do the job, so this method
3180
     * can be used if code sometimes calls the same record multiple times to save
3181
     * some queries. This should not be done if the calling code may change the
3182
     * same record meanwhile.
3183
     *
3184
     * @param string $table Tablename
3185
     * @param string $uid UID value
3186
     * @param string $pid PID value
3187
     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
3188
     */
3189
    public static function getTSCpidCached($table, $uid, $pid)
3190
    {
3191
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3192
        $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: [];
3193
        $key = $table . ':' . $uid . ':' . $pid;
3194
        if (!isset($firstLevelCache[$key])) {
3195
            $firstLevelCache[$key] = static::getTSCpid($table, $uid, $pid);
0 ignored issues
show
Bug introduced by
$pid of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

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

3195
            $firstLevelCache[$key] = static::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ $pid);
Loading history...
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

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

3195
            $firstLevelCache[$key] = static::getTSCpid($table, /** @scrutinizer ignore-type */ $uid, $pid);
Loading history...
3196
            $runtimeCache->set('backendUtilityTscPidCached', $firstLevelCache);
3197
        }
3198
        return $firstLevelCache[$key];
3199
    }
3200
3201
    /**
3202
     * 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.
3203
     *
3204
     * @param string $table Table name
3205
     * @param int $uid Record uid
3206
     * @param int $pid Record pid
3207
     * @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,
3208
     * second value is the PID value for TSconfig (uid if table is pages, otherwise the pid)
3209
     * @internal
3210
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::setHistory()
3211
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::process_datamap()
3212
     */
3213
    public static function getTSCpid($table, $uid, $pid)
3214
    {
3215
        // If pid is negative (referring to another record) the pid of the other record is fetched and returned.
3216
        $cPid = self::getTSconfig_pidValue($table, $uid, $pid);
3217
        // $TScID is the id of $table = pages, else it's the pid of the record.
3218
        $TScID = $table === 'pages' && MathUtility::canBeInterpretedAsInteger($uid) ? $uid : $cPid;
3219
        return [$TScID, $cPid];
3220
    }
3221
3222
    /**
3223
     * Returns soft-reference parser for the softRef processing type
3224
     * Usage: $softRefObj = BackendUtility::softRefParserObj('[parser key]');
3225
     *
3226
     * @param string $spKey softRef parser key
3227
     * @return mixed If available, returns Soft link parser object, otherwise false.
3228
     * @internal should only be used from within TYPO3 Core
3229
     */
3230
    public static function softRefParserObj($spKey)
3231
    {
3232
        $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser'][$spKey] ?? false;
3233
        if ($className) {
3234
            return GeneralUtility::makeInstance($className);
3235
        }
3236
        return false;
3237
    }
3238
3239
    /**
3240
     * Gets an instance of the runtime cache.
3241
     *
3242
     * @return FrontendInterface
3243
     */
3244
    protected static function getRuntimeCache()
3245
    {
3246
        return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3247
    }
3248
3249
    /**
3250
     * Returns array of soft parser references
3251
     *
3252
     * @param string $parserList softRef parser list
3253
     * @return array|bool Array where the parser key is the key and the value is the parameter string, FALSE if no parsers were found
3254
     * @throws \InvalidArgumentException
3255
     * @internal should only be used from within TYPO3 Core
3256
     */
3257
    public static function explodeSoftRefParserList($parserList)
3258
    {
3259
        // Return immediately if list is blank:
3260
        if ((string)$parserList === '') {
3261
            return false;
3262
        }
3263
3264
        $runtimeCache = self::getRuntimeCache();
3265
        $cacheId = 'backend-softRefList-' . md5($parserList);
3266
        $parserListCache = $runtimeCache->get($cacheId);
3267
        if ($parserListCache !== false) {
3268
            return $parserListCache;
3269
        }
3270
3271
        // Otherwise parse the list:
3272
        $keyList = GeneralUtility::trimExplode(',', $parserList, true);
3273
        $output = [];
3274
        foreach ($keyList as $val) {
3275
            $reg = [];
3276
            if (preg_match('/^([[:alnum:]_-]+)\\[(.*)\\]$/', $val, $reg)) {
3277
                $output[$reg[1]] = GeneralUtility::trimExplode(';', $reg[2], true);
3278
            } else {
3279
                $output[$val] = '';
3280
            }
3281
        }
3282
        $runtimeCache->set($cacheId, $output);
3283
        return $output;
3284
    }
3285
3286
    /**
3287
     * Returns TRUE if $modName is set and is found as a main- or submodule in $TBE_MODULES array
3288
     *
3289
     * @param string $modName Module name
3290
     * @return bool
3291
     */
3292
    public static function isModuleSetInTBE_MODULES($modName)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::isModuleSetInTBE_MODULES" is not in camel caps format
Loading history...
3293
    {
3294
        $loaded = [];
3295
        foreach ($GLOBALS['TBE_MODULES'] as $mkey => $list) {
3296
            $loaded[$mkey] = 1;
3297
            if (!is_array($list) && trim($list)) {
3298
                $subList = GeneralUtility::trimExplode(',', $list, true);
3299
                foreach ($subList as $skey) {
3300
                    $loaded[$mkey . '_' . $skey] = 1;
3301
                }
3302
            }
3303
        }
3304
        return $modName && isset($loaded[$modName]);
3305
    }
3306
3307
    /**
3308
     * Counting references to a record/file
3309
     *
3310
     * @param string $table Table name (or "_FILE" if its a file)
3311
     * @param string $ref Reference: If table, then int-uid, if _FILE, then file reference (relative to Environment::getPublicPath())
3312
     * @param string $msg Message with %s, eg. "There were %s records pointing to this file!
3313
     * @param string|null $count Reference count
3314
     * @return string Output string (or int count value if no msg string specified)
3315
     */
3316
    public static function referenceCount($table, $ref, $msg = '', $count = null)
3317
    {
3318
        if ($count === null) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
3319
3320
            // Build base query
3321
            $queryBuilder = static::getQueryBuilderForTable('sys_refindex');
3322
            $queryBuilder
3323
                ->count('*')
3324
                ->from('sys_refindex')
3325
                ->where(
3326
                    $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
3327
                    $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
3328
                );
3329
3330
            // Look up the path:
3331
            if ($table === '_FILE') {
3332
                if (!GeneralUtility::isFirstPartOfStr($ref, Environment::getPublicPath())) {
3333
                    return '';
3334
                }
3335
3336
                $ref = PathUtility::stripPathSitePrefix($ref);
3337
                $queryBuilder->andWhere(
3338
                    $queryBuilder->expr()->eq('ref_string', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_STR))
3339
                );
3340
            } else {
3341
                $queryBuilder->andWhere(
3342
                    $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT))
3343
                );
3344
                if ($table === 'sys_file') {
3345
                    $queryBuilder->andWhere($queryBuilder->expr()->neq('tablename', $queryBuilder->quote('sys_file_metadata')));
3346
                }
3347
            }
3348
3349
            $count = $queryBuilder->execute()->fetchColumn(0);
3350
        }
3351
3352
        if ($count) {
3353
            return $msg ? sprintf($msg, $count) : $count;
3354
        }
3355
        return $msg ? '' : 0;
3356
    }
3357
3358
    /**
3359
     * Counting translations of records
3360
     *
3361
     * @param string $table Table name
3362
     * @param string $ref Reference: the record's uid
3363
     * @param string $msg Message with %s, eg. "This record has %s translation(s) which will be deleted, too!
3364
     * @return string Output string (or int count value if no msg string specified)
3365
     */
3366
    public static function translationCount($table, $ref, $msg = '')
3367
    {
3368
        $count = null;
3369
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']
3370
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
3371
        ) {
3372
            $queryBuilder = static::getQueryBuilderForTable($table);
3373
            $queryBuilder->getRestrictions()
3374
                ->removeAll()
3375
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3376
3377
            $count = (int)$queryBuilder
3378
                ->count('*')
3379
                ->from($table)
3380
                ->where(
3381
                    $queryBuilder->expr()->eq(
3382
                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3383
                        $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)
3384
                    ),
3385
                    $queryBuilder->expr()->neq(
3386
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
3387
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3388
                    )
3389
                )
3390
                ->execute()
3391
                ->fetchColumn(0);
3392
        }
3393
3394
        if ($count && $msg) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $count 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...
3395
            return sprintf($msg, $count);
3396
        }
3397
3398
        if ($count) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $count 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...
3399
            return $msg ? sprintf($msg, $count) : $count;
3400
        }
3401
        return $msg ? '' : 0;
3402
    }
3403
3404
    /*******************************************
3405
     *
3406
     * Workspaces / Versioning
3407
     *
3408
     *******************************************/
3409
    /**
3410
     * Select all versions of a record, ordered by latest created version (uid DESC)
3411
     *
3412
     * @param string $table Table name to select from
3413
     * @param int $uid Record uid for which to find versions.
3414
     * @param string $fields Field list to select
3415
     * @param int|null $workspace Search in workspace ID and Live WS, if 0 search only in LiveWS, if NULL search in all WS.
3416
     * @param bool $includeDeletedRecords If set, deleted-flagged versions are included! (Only for clean-up script!)
3417
     * @param array $row The current record
3418
     * @return array|null Array of versions of table/uid
3419
     * @internal should only be used from within TYPO3 Core
3420
     */
3421
    public static function selectVersionsOfRecord(
3422
        $table,
3423
        $uid,
3424
        $fields = '*',
3425
        $workspace = 0,
3426
        $includeDeletedRecords = false,
3427
        $row = null
3428
    ) {
3429
        $realPid = 0;
3430
        $outputRows = [];
3431
        if (static::isTableWorkspaceEnabled($table)) {
3432
            if (is_array($row) && !$includeDeletedRecords) {
3433
                $row['_CURRENT_VERSION'] = true;
3434
                $realPid = $row['pid'];
3435
                $outputRows[] = $row;
3436
            } else {
3437
                // Select UID version:
3438
                $row = self::getRecord($table, $uid, $fields, '', !$includeDeletedRecords);
3439
                // Add rows to output array:
3440
                if ($row) {
3441
                    $row['_CURRENT_VERSION'] = true;
3442
                    $realPid = $row['pid'];
3443
                    $outputRows[] = $row;
3444
                }
3445
            }
3446
3447
            $queryBuilder = static::getQueryBuilderForTable($table);
3448
            $queryBuilder->getRestrictions()->removeAll();
3449
3450
            // build fields to select
3451
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3452
3453
            $queryBuilder
3454
                ->from($table)
3455
                ->where(
3456
                    $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
3457
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
3458
                )
3459
                ->orderBy('uid', 'DESC');
3460
3461
            if (!$includeDeletedRecords) {
3462
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3463
            }
3464
3465
            if ($workspace === 0) {
3466
                // Only in Live WS
3467
                $queryBuilder->andWhere(
3468
                    $queryBuilder->expr()->eq(
3469
                        't3ver_wsid',
3470
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3471
                    )
3472
                );
3473
            } elseif ($workspace !== null) {
3474
                // In Live WS and Workspace with given ID
3475
                $queryBuilder->andWhere(
3476
                    $queryBuilder->expr()->in(
3477
                        't3ver_wsid',
3478
                        $queryBuilder->createNamedParameter([0, (int)$workspace], Connection::PARAM_INT_ARRAY)
3479
                    )
3480
                );
3481
            }
3482
3483
            $rows = $queryBuilder->execute()->fetchAll();
3484
3485
            // Add rows to output array:
3486
            if (is_array($rows)) {
0 ignored issues
show
introduced by
The condition is_array($rows) is always true.
Loading history...
3487
                $outputRows = array_merge($outputRows, $rows);
3488
            }
3489
            // Set real-pid:
3490
            foreach ($outputRows as $idx => $oRow) {
3491
                $outputRows[$idx]['_REAL_PID'] = $realPid;
3492
            }
3493
            return $outputRows;
3494
        }
3495
        return null;
3496
    }
3497
3498
    /**
3499
     * Find page-tree PID for versionized record
3500
     * Will look if the "pid" value of the input record is -1 and if the table supports versioning - if so,
3501
     * it will translate the -1 PID into the PID of the original record
3502
     * Used whenever you are tracking something back, like making the root line.
3503
     * Will only translate if the workspace of the input record matches that of the current user (unless flag set)
3504
     * Principle; Record offline! => Find online?
3505
     *
3506
     * If the record had its pid corrected to the online versions pid, then "_ORIG_pid" is set
3507
     * to the original pid value (-1 of course). The field "_ORIG_pid" is used by various other functions
3508
     * to detect if a record was in fact in a versionized branch.
3509
     *
3510
     * @param string $table Table name
3511
     * @param array $rr Record array passed by reference. As minimum, "pid" and "uid" fields must exist! "t3ver_oid" and "t3ver_wsid" is nice and will save you a DB query.
3512
     * @param bool $ignoreWorkspaceMatch Ignore workspace match
3513
     * @see PageRepository::fixVersioningPid()
3514
     * @internal should only be used from within TYPO3 Core
3515
     */
3516
    public static function fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch = false)
3517
    {
3518
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3519
            return;
3520
        }
3521
        if (!static::isTableWorkspaceEnabled($table)) {
3522
            return;
3523
        }
3524
        // Check that the input record is an offline version from a table that supports versioning:
3525
        if (is_array($rr)) {
0 ignored issues
show
introduced by
The condition is_array($rr) is always true.
Loading history...
3526
            // Check values for t3ver_oid and t3ver_wsid:
3527
            if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid'])) {
3528
                // If "t3ver_oid" is already a field, just set this:
3529
                $oid = $rr['t3ver_oid'];
3530
                $wsid = $rr['t3ver_wsid'];
3531
            } else {
3532
                $oid = 0;
3533
                $wsid = 0;
3534
                // Otherwise we have to expect "uid" to be in the record and look up based on this:
3535
                $newPidRec = self::getRecord($table, $rr['uid'], 't3ver_oid,t3ver_wsid');
3536
                if (is_array($newPidRec)) {
3537
                    $oid = $newPidRec['t3ver_oid'];
3538
                    $wsid = $newPidRec['t3ver_wsid'];
3539
                }
3540
            }
3541
            // If ID of current online version is found, look up the PID value of that:
3542
            if ($oid
3543
                && ($ignoreWorkspaceMatch || (static::getBackendUserAuthentication() instanceof BackendUserAuthentication && (int)$wsid === (int)static::getBackendUserAuthentication()->workspace))
3544
            ) {
3545
                $oidRec = self::getRecord($table, $oid, 'pid');
3546
                if (is_array($oidRec)) {
3547
                    $rr['_ORIG_pid'] = $rr['pid'];
3548
                    $rr['pid'] = $oidRec['pid'];
3549
                }
3550
                // Use target PID in case of move pointer
3551
                if (
3552
                    !isset($rr['t3ver_state'])
3553
                    || VersionState::cast($rr['t3ver_state'])->equals(VersionState::MOVE_POINTER)
3554
                ) {
3555
                    $movePlaceholder = self::getMovePlaceholder($table, $oid, 'pid');
3556
                    if ($movePlaceholder) {
3557
                        $rr['_ORIG_pid'] = $rr['pid'];
3558
                        $rr['pid'] = $movePlaceholder['pid'];
3559
                    }
3560
                }
3561
            }
3562
        }
3563
    }
3564
3565
    /**
3566
     * Workspace Preview Overlay
3567
     * Generally ALWAYS used when records are selected based on uid or pid.
3568
     * If records are selected on other fields than uid or pid (eg. "email = ....")
3569
     * then usage might produce undesired results and that should be evaluated on individual basis.
3570
     * Principle; Record online! => Find offline?
3571
     * Recently, this function has been modified so it MAY set $row to FALSE.
3572
     * This happens if a version overlay with the move-id pointer is found in which case we would like a backend preview.
3573
     * In other words, you should check if the input record is still an array afterwards when using this function.
3574
     *
3575
     * @param string $table Table name
3576
     * @param array $row Record array passed by reference. As minimum, the "uid" and  "pid" fields must exist! Fake fields cannot exist since the fields in the array is used as field names in the SQL look up. It would be nice to have fields like "t3ver_state" and "t3ver_mode_id" as well to avoid a new lookup inside movePlhOL().
3577
     * @param int $wsid Workspace ID, if not specified will use static::getBackendUserAuthentication()->workspace
3578
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
3579
     * @see fixVersioningPid()
3580
     */
3581
    public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false)
3582
    {
3583
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3584
            return;
3585
        }
3586
        // If this is FALSE the placeholder is shown raw in the backend.
3587
        // I don't know if this move can be useful for users to toggle. Technically it can help debugging.
3588
        $previewMovePlaceholders = true;
3589
        // Initialize workspace ID
3590
        if ($wsid == -99 && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3591
            $wsid = static::getBackendUserAuthentication()->workspace;
3592
        }
3593
        // Check if workspace is different from zero and record is set:
3594
        if ($wsid !== 0 && is_array($row)) {
3595
            // Check if input record is a move-placeholder and if so, find the pointed-to live record:
3596
            $movePldSwap = null;
3597
            $orig_uid = 0;
3598
            $orig_pid = 0;
3599
            if ($previewMovePlaceholders) {
0 ignored issues
show
introduced by
The condition $previewMovePlaceholders is always true.
Loading history...
3600
                $orig_uid = $row['uid'];
3601
                $orig_pid = $row['pid'];
3602
                $movePldSwap = self::movePlhOL($table, $row);
3603
            }
3604
            $wsAlt = self::getWorkspaceVersionOfRecord(
3605
                $wsid,
3606
                $table,
3607
                $row['uid'],
3608
                implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3609
            );
3610
            // If version was found, swap the default record with that one.
3611
            if (is_array($wsAlt)) {
3612
                // Check if this is in move-state:
3613
                if ($previewMovePlaceholders && !$movePldSwap && static::isTableWorkspaceEnabled($table) && $unsetMovePointers) {
3614
                    // Only for WS ver 2... (moving)
3615
                    // If t3ver_state is not found, then find it... (but we like best if it is here...)
3616
                    if (!isset($wsAlt['t3ver_state'])) {
3617
                        $stateRec = self::getRecord($table, $wsAlt['uid'], 't3ver_state');
3618
                        $versionState = VersionState::cast($stateRec['t3ver_state']);
3619
                    } else {
3620
                        $versionState = VersionState::cast($wsAlt['t3ver_state']);
3621
                    }
3622
                    if ($versionState->equals(VersionState::MOVE_POINTER)) {
3623
                        // @todo Same problem as frontend in versionOL(). See TODO point there.
3624
                        $row = false;
3625
                        return;
3626
                    }
3627
                }
3628
                // Always correct PID from -1 to what it should be
3629
                if (isset($wsAlt['pid'])) {
3630
                    // Keep the old (-1) - indicates it was a version.
3631
                    $wsAlt['_ORIG_pid'] = $wsAlt['pid'];
3632
                    // Set in the online versions PID.
3633
                    $wsAlt['pid'] = $row['pid'];
3634
                }
3635
                // For versions of single elements or page+content, swap UID and PID
3636
                $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
3637
                $wsAlt['uid'] = $row['uid'];
3638
                // Backend css class:
3639
                $wsAlt['_CSSCLASS'] = 'ver-element';
3640
                // Changing input record to the workspace version alternative:
3641
                $row = $wsAlt;
3642
            }
3643
            // If the original record was a move placeholder, the uid and pid of that is preserved here:
3644
            if ($movePldSwap) {
3645
                $row['_MOVE_PLH'] = true;
3646
                $row['_MOVE_PLH_uid'] = $orig_uid;
3647
                $row['_MOVE_PLH_pid'] = $orig_pid;
3648
                // For display; To make the icon right for the placeholder vs. the original
3649
                $row['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
3650
            }
3651
        }
3652
    }
3653
3654
    /**
3655
     * Checks if record is a move-placeholder (t3ver_state==VersionState::MOVE_PLACEHOLDER) and if so
3656
     * it will set $row to be the pointed-to live record (and return TRUE)
3657
     *
3658
     * @param string $table Table name
3659
     * @param array $row Row (passed by reference) - must be online record!
3660
     * @return bool TRUE if overlay is made.
3661
     * @see PageRepository::movePlhOl()
3662
     * @internal should only be used from within TYPO3 Core
3663
     */
3664
    public static function movePlhOL($table, &$row)
3665
    {
3666
        if (static::isTableWorkspaceEnabled($table)) {
3667
            // If t3ver_move_id or t3ver_state is not found, then find it... (but we like best if it is here...)
3668
            if (!isset($row['t3ver_move_id']) || !isset($row['t3ver_state'])) {
3669
                $moveIDRec = self::getRecord($table, $row['uid'], 't3ver_move_id, t3ver_state');
3670
                $moveID = $moveIDRec['t3ver_move_id'];
3671
                $versionState = VersionState::cast($moveIDRec['t3ver_state']);
3672
            } else {
3673
                $moveID = $row['t3ver_move_id'];
3674
                $versionState = VersionState::cast($row['t3ver_state']);
3675
            }
3676
            // Find pointed-to record.
3677
            if ($versionState->equals(VersionState::MOVE_PLACEHOLDER) && $moveID) {
3678
                if ($origRow = self::getRecord(
3679
                    $table,
3680
                    $moveID,
3681
                    implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3682
                )) {
3683
                    $row = $origRow;
3684
                    return true;
3685
                }
3686
            }
3687
        }
3688
        return false;
3689
    }
3690
3691
    /**
3692
     * Select the workspace version of a record, if exists
3693
     *
3694
     * @param int $workspace Workspace ID
3695
     * @param string $table Table name to select from
3696
     * @param int $uid Record uid for which to find workspace version.
3697
     * @param string $fields Field list to select
3698
     * @return array|bool If found, return record, otherwise false
3699
     */
3700
    public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*')
3701
    {
3702
        if (ExtensionManagementUtility::isLoaded('workspaces')) {
3703
            if ($workspace !== 0 && self::isTableWorkspaceEnabled($table)) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
3704
3705
                // Select workspace version of record:
3706
                $queryBuilder = static::getQueryBuilderForTable($table);
3707
                $queryBuilder->getRestrictions()
3708
                    ->removeAll()
3709
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3710
3711
                // build fields to select
3712
                $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3713
3714
                $row = $queryBuilder
3715
                    ->from($table)
3716
                    ->where(
3717
                        $queryBuilder->expr()->eq(
3718
                            't3ver_oid',
3719
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3720
                        ),
3721
                        $queryBuilder->expr()->eq(
3722
                            't3ver_wsid',
3723
                            $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
3724
                        )
3725
                    )
3726
                    ->execute()
3727
                    ->fetch();
3728
3729
                return $row;
3730
            }
3731
        }
3732
        return false;
3733
    }
3734
3735
    /**
3736
     * Returns live version of record
3737
     *
3738
     * @param string $table Table name
3739
     * @param int $uid Record UID of draft, offline version
3740
     * @param string $fields Field list, default is *
3741
     * @return array|null If found, the record, otherwise NULL
3742
     */
3743
    public static function getLiveVersionOfRecord($table, $uid, $fields = '*')
3744
    {
3745
        $liveVersionId = self::getLiveVersionIdOfRecord($table, $uid);
3746
        if ($liveVersionId !== null) {
3747
            return self::getRecord($table, $liveVersionId, $fields);
3748
        }
3749
        return null;
3750
    }
3751
3752
    /**
3753
     * Gets the id of the live version of a record.
3754
     *
3755
     * @param string $table Name of the table
3756
     * @param int $uid Uid of the offline/draft record
3757
     * @return int|null The id of the live version of the record (or NULL if nothing was found)
3758
     * @internal should only be used from within TYPO3 Core
3759
     */
3760
    public static function getLiveVersionIdOfRecord($table, $uid)
3761
    {
3762
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3763
            return null;
3764
        }
3765
        $liveVersionId = null;
3766
        if (self::isTableWorkspaceEnabled($table)) {
3767
            $currentRecord = self::getRecord($table, $uid, 'pid,t3ver_oid');
3768
            if (is_array($currentRecord) && (int)$currentRecord['t3ver_oid'] > 0) {
3769
                $liveVersionId = $currentRecord['t3ver_oid'];
3770
            }
3771
        }
3772
        return $liveVersionId;
3773
    }
3774
3775
    /**
3776
     * Will return where clause de-selecting new(/deleted)-versions from other workspaces.
3777
     * If in live-workspace, don't show "MOVE-TO-PLACEHOLDERS" records if versioningWS is 2 (allows moving)
3778
     *
3779
     * @param string $table Table name
3780
     * @return string Where clause if applicable.
3781
     * @internal should only be used from within TYPO3 Core
3782
     */
3783
    public static function versioningPlaceholderClause($table)
3784
    {
3785
        if (static::isTableWorkspaceEnabled($table) && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3786
            $currentWorkspace = (int)static::getBackendUserAuthentication()->workspace;
3787
            return ' AND (' . $table . '.t3ver_state <= ' . new VersionState(VersionState::DEFAULT_STATE) . ' OR ' . $table . '.t3ver_wsid = ' . $currentWorkspace . ')';
3788
        }
3789
        return '';
3790
    }
3791
3792
    /**
3793
     * Get additional where clause to select records of a specific workspace (includes live as well).
3794
     *
3795
     * @param string $table Table name
3796
     * @param int $workspaceId Workspace ID
3797
     * @return string Workspace where clause
3798
     * @internal should only be used from within TYPO3 Core
3799
     */
3800
    public static function getWorkspaceWhereClause($table, $workspaceId = null)
3801
    {
3802
        $whereClause = '';
3803
        if (self::isTableWorkspaceEnabled($table) && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3804
            if ($workspaceId === null) {
3805
                $workspaceId = static::getBackendUserAuthentication()->workspace;
3806
            }
3807
            $workspaceId = (int)$workspaceId;
3808
            $comparison = $workspaceId === 0 ? '=' : '>';
3809
            $whereClause = ' AND ' . $table . '.t3ver_wsid=' . $workspaceId . ' AND ' . $table . '.t3ver_oid' . $comparison . '0';
3810
        }
3811
        return $whereClause;
3812
    }
3813
3814
    /**
3815
     * Performs mapping of new uids to new versions UID in case of import inside a workspace.
3816
     *
3817
     * @param string $table Table name
3818
     * @param int $uid Record uid (of live record placeholder)
3819
     * @return int Uid of offline version if any, otherwise live uid.
3820
     * @internal should only be used from within TYPO3 Core
3821
     */
3822
    public static function wsMapId($table, $uid)
3823
    {
3824
        $wsRec = null;
3825
        if (static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
0 ignored issues
show
introduced by
static::getBackendUserAuthentication() is always a sub-type of TYPO3\CMS\Core\Authentic...ckendUserAuthentication.
Loading history...
3826
            $wsRec = self::getWorkspaceVersionOfRecord(
3827
                static::getBackendUserAuthentication()->workspace,
3828
                $table,
3829
                $uid,
3830
                'uid'
3831
            );
3832
        }
3833
        return is_array($wsRec) ? $wsRec['uid'] : $uid;
3834
    }
3835
3836
    /**
3837
     * Returns move placeholder of online (live) version
3838
     *
3839
     * @param string $table Table name
3840
     * @param int $uid Record UID of online version
3841
     * @param string $fields Field list, default is *
3842
     * @param int|null $workspace The workspace to be used
3843
     * @return array|bool If found, the record, otherwise false
3844
     * @internal should only be used from within TYPO3 Core
3845
     */
3846
    public static function getMovePlaceholder($table, $uid, $fields = '*', $workspace = null)
3847
    {
3848
        if ($workspace === null && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3849
            $workspace = static::getBackendUserAuthentication()->workspace;
3850
        }
3851
        if ((int)$workspace !== 0 && static::isTableWorkspaceEnabled($table)) {
3852
            // Select workspace version of record:
3853
            $queryBuilder = static::getQueryBuilderForTable($table);
3854
            $queryBuilder->getRestrictions()
3855
                ->removeAll()
3856
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3857
3858
            $row = $queryBuilder
3859
                ->select(...GeneralUtility::trimExplode(',', $fields, true))
3860
                ->from($table)
3861
                ->where(
3862
                    $queryBuilder->expr()->eq(
3863
                        't3ver_state',
3864
                        $queryBuilder->createNamedParameter(
3865
                            (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
3866
                            \PDO::PARAM_INT
3867
                        )
3868
                    ),
3869
                    $queryBuilder->expr()->eq(
3870
                        't3ver_move_id',
3871
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3872
                    ),
3873
                    $queryBuilder->expr()->eq(
3874
                        't3ver_wsid',
3875
                        $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
3876
                    )
3877
                )
3878
                ->execute()
3879
                ->fetch();
3880
3881
            return $row ?: false;
3882
        }
3883
        return false;
3884
    }
3885
3886
    /*******************************************
3887
     *
3888
     * Miscellaneous
3889
     *
3890
     *******************************************/
3891
    /**
3892
     * Prints TYPO3 Copyright notice for About Modules etc. modules.
3893
     *
3894
     * Warning:
3895
     * DO NOT prevent this notice from being shown in ANY WAY.
3896
     * According to the GPL license an interactive application must show such a notice on start-up ('If the program is interactive, make it output a short notice... ' - see GPL.txt)
3897
     * Therefore preventing this notice from being properly shown is a violation of the license, regardless of whether you remove it or use a stylesheet to obstruct the display.
3898
     *
3899
     * @return string Text/Image (HTML) for copyright notice.
3900
     * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11.0
3901
     */
3902
    public static function TYPO3_copyRightNotice()
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::TYPO3_copyRightNotice" is not in camel caps format
Loading history...
3903
    {
3904
        trigger_error('BackendUtility::TYPO3_copyRightNotice() will be removed in TYPO3 v11.0, use the Typo3Information PHP class instead.', E_USER_DEPRECATED);
3905
        $copyrightGenerator = GeneralUtility::makeInstance(Typo3Information::class, static::getLanguageService());
3906
        return $copyrightGenerator->getCopyrightNotice();
3907
    }
3908
3909
    /**
3910
     * Creates ADMCMD parameters for the "viewpage" extension / frontend
3911
     *
3912
     * @param array $pageInfo Page record
3913
     * @param \TYPO3\CMS\Core\Context\Context $context
3914
     * @return string Query-parameters
3915
     * @internal
3916
     */
3917
    public static function ADMCMD_previewCmds($pageInfo, Context $context)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::ADMCMD_previewCmds" is not in camel caps format
Loading history...
3918
    {
3919
        $simUser = '';
3920
        $simTime = '';
3921
        if ($pageInfo['fe_group'] > 0) {
3922
            $simUser = '&ADMCMD_simUser=' . $pageInfo['fe_group'];
3923
        } elseif ((int)$pageInfo['fe_group'] === -2) {
3924
            // -2 means "show at any login". We simulate first available fe_group.
3925
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3926
                ->getQueryBuilderForTable('fe_groups');
3927
            $queryBuilder->getRestrictions()
3928
                ->removeAll()
3929
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3930
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
3931
3932
            $activeFeGroupRow = $queryBuilder->select('uid')
3933
                ->from('fe_groups')
3934
                ->execute()
3935
                ->fetch();
3936
3937
            if (!empty($activeFeGroupRow)) {
3938
                $simUser = '&ADMCMD_simUser=' . $activeFeGroupRow['uid'];
3939
            }
3940
        }
3941
        $startTime = (int)$pageInfo['starttime'];
3942
        $endTime = (int)$pageInfo['endtime'];
3943
        if ($startTime > $GLOBALS['EXEC_TIME']) {
3944
            // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
3945
            // an URL for it
3946
            $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $startTime));
3947
            $context->setAspect('date', $dateAspect);
3948
            $simTime = '&ADMCMD_simTime=' . $startTime;
3949
        }
3950
        if ($endTime < $GLOBALS['EXEC_TIME'] && $endTime !== 0) {
3951
            // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
3952
            // in turn PageRouter will generate an URL for it
3953
            $dateAspect = GeneralUtility::makeInstance(
3954
                DateTimeAspect::class,
3955
                new \DateTimeImmutable('@' . ($endTime - 1))
3956
            );
3957
            $context->setAspect('date', $dateAspect);
3958
            $simTime = '&ADMCMD_simTime=' . ($endTime - 1);
3959
        }
3960
        return $simUser . $simTime;
3961
    }
3962
3963
    /**
3964
     * Returns the name of the backend script relative to the TYPO3 main directory.
3965
     *
3966
     * @param string $interface Name of the backend interface  (backend, frontend) to look up the script name for. If no interface is given, the interface for the current backend user is used.
3967
     * @return string The name of the backend script relative to the TYPO3 main directory.
3968
     * @internal should only be used from within TYPO3 Core
3969
     */
3970
    public static function getBackendScript($interface = '')
3971
    {
3972
        if (!$interface) {
3973
            $interface = static::getBackendUserAuthentication()->uc['interfaceSetup'];
3974
        }
3975
        switch ($interface) {
3976
            case 'frontend':
3977
                $script = '../.';
3978
                break;
3979
            case 'backend':
3980
            default:
3981
                $script = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('main');
3982
        }
3983
        return $script;
3984
    }
3985
3986
    /**
3987
     * Determines whether a table is enabled for workspaces.
3988
     *
3989
     * @param string $table Name of the table to be checked
3990
     * @return bool
3991
     */
3992
    public static function isTableWorkspaceEnabled($table)
3993
    {
3994
        return !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']);
3995
    }
3996
3997
    /**
3998
     * Gets the TCA configuration of a field.
3999
     *
4000
     * @param string $table Name of the table
4001
     * @param string $field Name of the field
4002
     * @return array
4003
     */
4004
    public static function getTcaFieldConfiguration($table, $field)
4005
    {
4006
        $configuration = [];
4007
        if (isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4008
            $configuration = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4009
        }
4010
        return $configuration;
4011
    }
4012
4013
    /**
4014
     * Whether to ignore restrictions on a web-mount of a table.
4015
     * The regular behaviour is that records to be accessed need to be
4016
     * in a valid user's web-mount.
4017
     *
4018
     * @param string $table Name of the table
4019
     * @return bool
4020
     */
4021
    public static function isWebMountRestrictionIgnored($table)
4022
    {
4023
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreWebMountRestriction']);
4024
    }
4025
4026
    /**
4027
     * Whether to ignore restrictions on root-level records.
4028
     * The regular behaviour is that records on the root-level (page-id 0)
4029
     * only can be accessed by admin users.
4030
     *
4031
     * @param string $table Name of the table
4032
     * @return bool
4033
     */
4034
    public static function isRootLevelRestrictionIgnored($table)
4035
    {
4036
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']);
4037
    }
4038
4039
    /**
4040
     * @param string $table
4041
     * @return Connection
4042
     */
4043
    protected static function getConnectionForTable($table)
4044
    {
4045
        return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
4046
    }
4047
4048
    /**
4049
     * @param string $table
4050
     * @return QueryBuilder
4051
     */
4052
    protected static function getQueryBuilderForTable($table)
4053
    {
4054
        return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4055
    }
4056
4057
    /**
4058
     * @return LoggerInterface
4059
     */
4060
    protected static function getLogger()
4061
    {
4062
        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
4063
    }
4064
4065
    /**
4066
     * @return LanguageService
4067
     */
4068
    protected static function getLanguageService()
4069
    {
4070
        return $GLOBALS['LANG'];
4071
    }
4072
4073
    /**
4074
     * @return BackendUserAuthentication
4075
     */
4076
    protected static function getBackendUserAuthentication()
4077
    {
4078
        return $GLOBALS['BE_USER'] ?? null;
4079
    }
4080
}
4081