Completed
Push — master ( 301171...c84954 )
by
unknown
29:11 queued 15:53
created

BackendUtility::getRuntimeCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
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\Log\LoggerInterface;
19
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
20
use TYPO3\CMS\Backend\Controller\File\ThumbnailController;
21
use TYPO3\CMS\Backend\Routing\UriBuilder;
22
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23
use TYPO3\CMS\Core\Cache\CacheManager;
24
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
25
use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
26
use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
27
use TYPO3\CMS\Core\Context\Context;
28
use TYPO3\CMS\Core\Context\DateTimeAspect;
29
use TYPO3\CMS\Core\Core\Environment;
30
use TYPO3\CMS\Core\Database\Connection;
31
use TYPO3\CMS\Core\Database\ConnectionPool;
32
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
33
use TYPO3\CMS\Core\Database\Query\QueryHelper;
34
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
35
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
36
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
37
use TYPO3\CMS\Core\Database\RelationHandler;
38
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
39
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
40
use TYPO3\CMS\Core\Imaging\Icon;
41
use TYPO3\CMS\Core\Imaging\IconFactory;
42
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
43
use TYPO3\CMS\Core\Localization\LanguageService;
44
use TYPO3\CMS\Core\Log\LogManager;
45
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
46
use TYPO3\CMS\Core\Resource\ProcessedFile;
47
use TYPO3\CMS\Core\Resource\ResourceFactory;
48
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
49
use TYPO3\CMS\Core\Routing\RouterInterface;
50
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
51
use TYPO3\CMS\Core\Site\SiteFinder;
52
use TYPO3\CMS\Core\Type\Bitmask\Permission;
53
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
54
use TYPO3\CMS\Core\Utility\ArrayUtility;
55
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
56
use TYPO3\CMS\Core\Utility\GeneralUtility;
57
use TYPO3\CMS\Core\Utility\HttpUtility;
58
use TYPO3\CMS\Core\Utility\MathUtility;
59
use TYPO3\CMS\Core\Utility\PathUtility;
60
use TYPO3\CMS\Core\Versioning\VersionState;
61
62
/**
63
 * Standard functions available for the TYPO3 backend.
64
 * You are encouraged to use this class in your own applications (Backend Modules)
65
 * Don't instantiate - call functions with "\TYPO3\CMS\Backend\Utility\BackendUtility::" prefixed the function name.
66
 *
67
 * Call ALL methods without making an object!
68
 * Eg. to get a page-record 51 do this: '\TYPO3\CMS\Backend\Utility\BackendUtility::getRecord('pages',51)'
69
 */
70
class BackendUtility
71
{
72
    /*******************************************
73
     *
74
     * SQL-related, selecting records, searching
75
     *
76
     *******************************************/
77
    /**
78
     * Gets record with uid = $uid from $table
79
     * You can set $field to a list of fields (default is '*')
80
     * Additional WHERE clauses can be added by $where (fx. ' AND some_field = 1')
81
     * Will automatically check if records has been deleted and if so, not return anything.
82
     * $table must be found in $GLOBALS['TCA']
83
     *
84
     * @param string $table Table name present in $GLOBALS['TCA']
85
     * @param int $uid UID of record
86
     * @param string $fields List of fields to select
87
     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
88
     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
89
     * @return array|null Returns the row if found, otherwise NULL
90
     */
91
    public static function getRecord($table, $uid, $fields = '*', $where = '', $useDeleteClause = true)
92
    {
93
        // Ensure we have a valid uid (not 0 and not NEWxxxx) and a valid TCA
94
        if ((int)$uid && !empty($GLOBALS['TCA'][$table])) {
95
            $queryBuilder = static::getQueryBuilderForTable($table);
96
97
            // do not use enabled fields here
98
            $queryBuilder->getRestrictions()->removeAll();
99
100
            // should the delete clause be used
101
            if ($useDeleteClause) {
102
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
103
            }
104
105
            // set table and where clause
106
            $queryBuilder
107
                ->select(...GeneralUtility::trimExplode(',', $fields, true))
108
                ->from($table)
109
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)));
110
111
            // add custom where clause
112
            if ($where) {
113
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
114
            }
115
116
            $row = $queryBuilder->execute()->fetch();
117
            if ($row) {
118
                return $row;
119
            }
120
        }
121
        return null;
122
    }
123
124
    /**
125
     * Like getRecord(), but overlays workspace version if any.
126
     *
127
     * @param string $table Table name present in $GLOBALS['TCA']
128
     * @param int $uid UID of record
129
     * @param string $fields List of fields to select
130
     * @param string $where Additional WHERE clause, eg. ' AND some_field = 0'
131
     * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
132
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
133
     * @return array Returns the row if found, otherwise nothing
134
     */
135
    public static function getRecordWSOL(
136
        $table,
137
        $uid,
138
        $fields = '*',
139
        $where = '',
140
        $useDeleteClause = true,
141
        $unsetMovePointers = false
142
    ) {
143
        if ($fields !== '*') {
144
            $internalFields = GeneralUtility::uniqueList($fields . ',uid,pid');
145
            $row = self::getRecord($table, $uid, $internalFields, $where, $useDeleteClause);
146
            self::workspaceOL($table, $row, -99, $unsetMovePointers);
147
            if (is_array($row)) {
148
                foreach ($row as $key => $_) {
149
                    if (!GeneralUtility::inList($fields, $key) && $key[0] !== '_') {
150
                        unset($row[$key]);
151
                    }
152
                }
153
            }
154
        } else {
155
            $row = self::getRecord($table, $uid, $fields, $where, $useDeleteClause);
156
            self::workspaceOL($table, $row, -99, $unsetMovePointers);
157
        }
158
        return $row;
159
    }
160
161
    /**
162
     * Purges computed properties starting with underscore character ('_').
163
     *
164
     * @param array $record
165
     * @return array
166
     * @internal should only be used from within TYPO3 Core
167
     */
168
    public static function purgeComputedPropertiesFromRecord(array $record): array
169
    {
170
        return array_filter(
171
            $record,
172
            function (string $propertyName): bool {
173
                return $propertyName[0] !== '_';
174
            },
175
            ARRAY_FILTER_USE_KEY
176
        );
177
    }
178
179
    /**
180
     * Purges computed property names starting with underscore character ('_').
181
     *
182
     * @param array $propertyNames
183
     * @return array
184
     * @internal should only be used from within TYPO3 Core
185
     */
186
    public static function purgeComputedPropertyNames(array $propertyNames): array
187
    {
188
        return array_filter(
189
            $propertyNames,
190
            function (string $propertyName): bool {
191
                return $propertyName[0] !== '_';
192
            }
193
        );
194
    }
195
196
    /**
197
     * Makes a backwards explode on the $str and returns an array with ($table, $uid).
198
     * Example: tt_content_45 => ['tt_content', 45]
199
     *
200
     * @param string $str [tablename]_[uid] string to explode
201
     * @return array
202
     * @internal should only be used from within TYPO3 Core
203
     */
204
    public static function splitTable_Uid($str)
205
    {
206
        [$uid, $table] = explode('_', strrev($str), 2);
207
        return [strrev($table), strrev($uid)];
208
    }
209
210
    /**
211
     * Backend implementation of enableFields()
212
     * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime.
213
     * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition.
214
     * $GLOBALS["SIM_ACCESS_TIME"] is used for date.
215
     *
216
     * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA'].
217
     * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection)
218
     * @return string WHERE clause part
219
     * @internal should only be used from within TYPO3 Core, but DefaultRestrictionHandler is recommended as alternative
220
     */
221
    public static function BEenableFields($table, $inv = false)
222
    {
223
        $ctrl = $GLOBALS['TCA'][$table]['ctrl'];
224
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
225
            ->getConnectionForTable($table)
226
            ->getExpressionBuilder();
227
        $query = $expressionBuilder->andX();
228
        $invQuery = $expressionBuilder->orX();
229
230
        if (is_array($ctrl)) {
231
            if (is_array($ctrl['enablecolumns'])) {
232
                if ($ctrl['enablecolumns']['disabled'] ?? false) {
233
                    $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
234
                    $query->add($expressionBuilder->eq($field, 0));
235
                    $invQuery->add($expressionBuilder->neq($field, 0));
236
                }
237
                if ($ctrl['enablecolumns']['starttime'] ?? false) {
238
                    $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
239
                    $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']));
240
                    $invQuery->add(
241
                        $expressionBuilder->andX(
242
                            $expressionBuilder->neq($field, 0),
243
                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
244
                        )
245
                    );
246
                }
247
                if ($ctrl['enablecolumns']['endtime'] ?? false) {
248
                    $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
249
                    $query->add(
250
                        $expressionBuilder->orX(
251
                            $expressionBuilder->eq($field, 0),
252
                            $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
253
                        )
254
                    );
255
                    $invQuery->add(
256
                        $expressionBuilder->andX(
257
                            $expressionBuilder->neq($field, 0),
258
                            $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
259
                        )
260
                    );
261
                }
262
            }
263
        }
264
265
        if ($query->count() === 0) {
266
            return '';
267
        }
268
269
        return ' AND ' . ($inv ? $invQuery : $query);
270
    }
271
272
    /**
273
     * Fetches the localization for a given record.
274
     *
275
     * @param string $table Table name present in $GLOBALS['TCA']
276
     * @param int $uid The uid of the record
277
     * @param int $language The uid of the language record in sys_language
278
     * @param string $andWhereClause Optional additional WHERE clause (default: '')
279
     * @return mixed Multidimensional array with selected records, empty array if none exists and FALSE if table is not localizable
280
     */
281
    public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
282
    {
283
        $recordLocalization = false;
284
285
        if (self::isTableLocalizable($table)) {
286
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
287
288
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
289
                ->getQueryBuilderForTable($table);
290
            $queryBuilder->getRestrictions()
291
                ->removeAll()
292
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
293
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace ?? 0));
294
295
            $queryBuilder->select('*')
296
                ->from($table)
297
                ->where(
298
                    $queryBuilder->expr()->eq(
299
                        $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'],
300
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
301
                    ),
302
                    $queryBuilder->expr()->eq(
303
                        $tcaCtrl['languageField'],
304
                        $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
305
                    )
306
                )
307
                ->setMaxResults(1);
308
309
            if ($andWhereClause) {
310
                $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
311
            }
312
313
            $recordLocalization = $queryBuilder->execute()->fetchAll();
314
        }
315
316
        return $recordLocalization;
317
    }
318
319
    /*******************************************
320
     *
321
     * Page tree, TCA related
322
     *
323
     *******************************************/
324
    /**
325
     * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id
326
     * ($uid) and back to the root.
327
     * By default deleted pages are filtered.
328
     * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known
329
     * from the frontend where the rootline stops when a root-template is found.
330
     *
331
     * @param int $uid Page id for which to create the root line.
332
     * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that
333
     *          stops the process if we meet a page, the user has no reading access to.
334
     * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is
335
     *          usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
336
     * @param string[] $additionalFields Additional Fields to select for rootline records
337
     * @return array Root line array, all the way to the page tree root uid=0 (or as far as $clause allows!), including the page given as $uid
338
     */
339
    public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = [])
340
    {
341
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
342
        $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: [];
343
        $output = [];
344
        $pid = $uid;
345
        $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . md5(implode(',', $additionalFields)) : '');
346
        if (is_array($beGetRootLineCache[$ident] ?? false)) {
347
            $output = $beGetRootLineCache[$ident];
348
        } else {
349
            $loopCheck = 100;
350
            $theRowArray = [];
351
            while ($uid != 0 && $loopCheck) {
352
                $loopCheck--;
353
                $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields);
354
                if (is_array($row)) {
355
                    $uid = $row['pid'];
356
                    $theRowArray[] = $row;
357
                } else {
358
                    break;
359
                }
360
            }
361
            $fields = [
362
                'uid',
363
                'pid',
364
                'title',
365
                'doktype',
366
                'slug',
367
                'tsconfig_includes',
368
                'TSconfig',
369
                'is_siteroot',
370
                't3ver_oid',
371
                't3ver_wsid',
372
                't3ver_state',
373
                't3ver_stage',
374
                'backend_layout_next_level',
375
                'hidden',
376
                'starttime',
377
                'endtime',
378
                'fe_group',
379
                'nav_hide',
380
                'content_from_pid',
381
                'module',
382
                'extendToSubpages'
383
            ];
384
            $fields = array_merge($fields, $additionalFields);
385
            $rootPage = array_fill_keys($fields, null);
386
            if ($uid == 0) {
387
                $rootPage['uid'] = 0;
388
                $theRowArray[] = $rootPage;
389
            }
390
            $c = count($theRowArray);
391
            foreach ($theRowArray as $val) {
392
                $c--;
393
                $output[$c] = array_intersect_key($val, $rootPage);
394
                if (isset($val['_ORIG_pid'])) {
395
                    $output[$c]['_ORIG_pid'] = $val['_ORIG_pid'];
396
                }
397
            }
398
            $beGetRootLineCache[$ident] = $output;
399
            $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache);
400
        }
401
        return $output;
402
    }
403
404
    /**
405
     * Gets the cached page record for the rootline
406
     *
407
     * @param int $uid Page id for which to create the root line.
408
     * @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.
409
     * @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!
410
     * @param string[] $additionalFields AdditionalFields to fetch from the root line
411
     * @return array Cached page record for the rootline
412
     * @see BEgetRootLine
413
     */
414
    protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = [])
415
    {
416
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
417
        $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: [];
418
        $statementCacheIdent = md5($clause . ($additionalFields ? '-' . implode(',', $additionalFields) : ''));
419
        $ident = $uid . '-' . $workspaceOL . '-' . $statementCacheIdent;
420
        if (is_array($pageForRootlineCache[$ident] ?? false)) {
421
            $row = $pageForRootlineCache[$ident];
422
        } else {
423
            $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
424
            if (!$statement) {
425
                $queryBuilder = static::getQueryBuilderForTable('pages');
426
                $queryBuilder->getRestrictions()
427
                             ->removeAll()
428
                             ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
429
430
                $queryBuilder
431
                    ->select(
432
                        'pid',
433
                        'uid',
434
                        'title',
435
                        'doktype',
436
                        'slug',
437
                        'tsconfig_includes',
438
                        'TSconfig',
439
                        'is_siteroot',
440
                        't3ver_oid',
441
                        't3ver_wsid',
442
                        't3ver_state',
443
                        't3ver_stage',
444
                        'backend_layout_next_level',
445
                        'hidden',
446
                        'starttime',
447
                        'endtime',
448
                        'fe_group',
449
                        'nav_hide',
450
                        'content_from_pid',
451
                        'module',
452
                        'extendToSubpages',
453
                        ...$additionalFields
454
                    )
455
                    ->from('pages')
456
                    ->where(
457
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)),
458
                        QueryHelper::stripLogicalOperatorPrefix($clause)
459
                    );
460
                $statement = $queryBuilder->execute();
461
                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement);
462
            } else {
463
                $statement->bindValue(1, (int)$uid);
464
                $statement->execute();
465
            }
466
            $row = $statement->fetch();
467
            $statement->closeCursor();
468
469
            if ($row) {
470
                if ($workspaceOL) {
471
                    self::workspaceOL('pages', $row);
472
                }
473
                if (is_array($row)) {
474
                    $pageForRootlineCache[$ident] = $row;
475
                    $runtimeCache->set('backendUtilityPageForRootLine', $pageForRootlineCache);
476
                }
477
            }
478
        }
479
        return $row;
480
    }
481
482
    /**
483
     * Opens the page tree to the specified page id
484
     *
485
     * @param int $pid Page id.
486
     * @param bool $clearExpansion If set, then other open branches are closed.
487
     * @internal should only be used from within TYPO3 Core
488
     */
489
    public static function openPageTree($pid, $clearExpansion)
490
    {
491
        $beUser = static::getBackendUserAuthentication();
492
        // Get current expansion data:
493
        if ($clearExpansion) {
494
            $expandedPages = [];
495
        } else {
496
            $expandedPages = json_decode($beUser->uc['browseTrees']['browsePages'], true);
497
        }
498
        // Get rootline:
499
        $rL = self::BEgetRootLine($pid);
500
        // First, find out what mount index to use (if more than one DB mount exists):
501
        $mountIndex = 0;
502
        $mountKeys = array_flip($beUser->returnWebmounts());
503
        foreach ($rL as $rLDat) {
504
            if (isset($mountKeys[$rLDat['uid']])) {
505
                $mountIndex = $mountKeys[$rLDat['uid']];
506
                break;
507
            }
508
        }
509
        // Traverse rootline and open paths:
510
        foreach ($rL as $rLDat) {
511
            $expandedPages[$mountIndex][$rLDat['uid']] = 1;
512
        }
513
        // Write back:
514
        $beUser->uc['browseTrees']['browsePages'] = json_encode($expandedPages);
515
        $beUser->writeUC();
516
    }
517
518
    /**
519
     * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage"
520
     * Each part of the path will be limited to $titleLimit characters
521
     * Deleted pages are filtered out.
522
     *
523
     * @param int $uid Page uid for which to create record path
524
     * @param string $clause Clause is additional where clauses, eg.
525
     * @param int $titleLimit Title limit
526
     * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so)
527
     * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set.
528
     */
529
    public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0)
530
    {
531
        if (!$titleLimit) {
532
            $titleLimit = 1000;
533
        }
534
        $output = $fullOutput = '/';
535
        $clause = trim($clause);
536
        if ($clause !== '' && strpos($clause, 'AND') !== 0) {
537
            $clause = 'AND ' . $clause;
538
        }
539
        $data = self::BEgetRootLine($uid, $clause, true);
540
        foreach ($data as $record) {
541
            if ($record['uid'] === 0) {
542
                continue;
543
            }
544
            $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output;
545
            if ($fullTitleLimit) {
546
                $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput;
547
            }
548
        }
549
        if ($fullTitleLimit) {
550
            return [$output, $fullOutput];
551
        }
552
        return $output;
553
    }
554
555
    /**
556
     * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA'].
557
     *
558
     * @param string $table The table to check
559
     * @return bool Whether a table is localizable
560
     */
561
    public static function isTableLocalizable($table)
562
    {
563
        $isLocalizable = false;
564
        if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) {
565
            $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
566
            $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField'];
567
        }
568
        return $isLocalizable;
569
    }
570
571
    /**
572
     * 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.
573
     * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin.
574
     * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause)
575
     *
576
     * @param int $id Page uid for which to check read-access
577
     * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1);
578
     * @return array|bool Returns page record if OK, otherwise FALSE.
579
     */
580
    public static function readPageAccess($id, $perms_clause)
581
    {
582
        if ((string)$id !== '') {
583
            $id = (int)$id;
584
            if (!$id) {
585
                if (static::getBackendUserAuthentication()->isAdmin()) {
586
                    return ['_thePath' => '/'];
587
                }
588
            } else {
589
                $pageinfo = self::getRecord('pages', $id, '*', $perms_clause);
590
                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...
591
                    self::workspaceOL('pages', $pageinfo);
592
                    if (is_array($pageinfo)) {
593
                        [$pageinfo['_thePath'], $pageinfo['_thePathFull']] = self::getRecordPath((int)$pageinfo['uid'], $perms_clause, 15, 1000);
594
                        return $pageinfo;
595
                    }
596
                }
597
            }
598
        }
599
        return false;
600
    }
601
602
    /**
603
     * Returns the "type" value of $rec from $table which can be used to look up the correct "types" rendering section in $GLOBALS['TCA']
604
     * If no "type" field is configured in the "ctrl"-section of the $GLOBALS['TCA'] for the table, zero is used.
605
     * 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)
606
     *
607
     * Note: This method is very similar to the type determination of FormDataProvider/DatabaseRecordTypeValue,
608
     * however, it has two differences:
609
     * 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).
610
     * 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"
611
     * and "group" field values are stored, which makes different processing of the "foreign pointer field" type field variant necessary.
612
     *
613
     * @param string $table Table name present in TCA
614
     * @param array $row Record from $table
615
     * @throws \RuntimeException
616
     * @return string Field value
617
     */
618
    public static function getTCAtypeValue($table, $row)
619
    {
620
        $typeNum = 0;
621
        if ($GLOBALS['TCA'][$table]) {
622
            $field = $GLOBALS['TCA'][$table]['ctrl']['type'];
623
            if (strpos($field, ':') !== false) {
624
                [$pointerField, $foreignTableTypeField] = explode(':', $field);
625
                // Get field value from database if field is not in the $row array
626
                if (!isset($row[$pointerField])) {
627
                    $localRow = self::getRecord($table, $row['uid'], $pointerField);
628
                    $foreignUid = $localRow[$pointerField];
629
                } else {
630
                    $foreignUid = $row[$pointerField];
631
                }
632
                if ($foreignUid) {
633
                    $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
634
                    $relationType = $fieldConfig['type'];
635
                    if ($relationType === 'select') {
636
                        $foreignTable = $fieldConfig['foreign_table'];
637
                    } elseif ($relationType === 'group') {
638
                        $allowedTables = explode(',', $fieldConfig['allowed']);
639
                        $foreignTable = $allowedTables[0];
640
                    } else {
641
                        throw new \RuntimeException(
642
                            'TCA foreign field pointer fields are only allowed to be used with group or select field types.',
643
                            1325862240
644
                        );
645
                    }
646
                    $foreignRow = self::getRecord($foreignTable, $foreignUid, $foreignTableTypeField);
647
                    if ($foreignRow[$foreignTableTypeField]) {
648
                        $typeNum = $foreignRow[$foreignTableTypeField];
649
                    }
650
                }
651
            } else {
652
                $typeNum = $row[$field];
653
            }
654
            // If that value is an empty string, set it to "0" (zero)
655
            if (empty($typeNum)) {
656
                $typeNum = 0;
657
            }
658
        }
659
        // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
660
        if (!isset($GLOBALS['TCA'][$table]['types'][$typeNum]) || !$GLOBALS['TCA'][$table]['types'][$typeNum]) {
661
            $typeNum = isset($GLOBALS['TCA'][$table]['types']['0']) ? 0 : 1;
662
        }
663
        // Force to string. Necessary for eg '-1' to be recognized as a type value.
664
        $typeNum = (string)$typeNum;
665
        return $typeNum;
666
    }
667
668
    /*******************************************
669
     *
670
     * TypoScript related
671
     *
672
     *******************************************/
673
    /**
674
     * Returns the Page TSconfig for page with id, $id
675
     *
676
     * @param int $id Page uid for which to create Page TSconfig
677
     * @return array Page TSconfig
678
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
679
     */
680
    public static function getPagesTSconfig($id)
681
    {
682
        $id = (int)$id;
683
684
        $cache = self::getRuntimeCache();
685
        $pagesTsConfigIdToHash = $cache->get('pagesTsConfigIdToHash' . $id);
686
        if ($pagesTsConfigIdToHash !== false) {
687
            return $cache->get('pagesTsConfigHashToContent' . $pagesTsConfigIdToHash);
688
        }
689
690
        $rootLine = self::BEgetRootLine($id, '', true);
691
        // Order correctly
692
        ksort($rootLine);
693
694
        try {
695
            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id);
696
        } catch (SiteNotFoundException $exception) {
697
            $site = null;
698
        }
699
700
        // Load PageTS from all pages of the rootLine
701
        $pageTs = GeneralUtility::makeInstance(PageTsConfigLoader::class)->load($rootLine);
702
703
        // Parse the PageTS into an array, also applying conditions
704
        $parser = GeneralUtility::makeInstance(
705
            PageTsConfigParser::class,
706
            GeneralUtility::makeInstance(TypoScriptParser::class),
707
            GeneralUtility::makeInstance(CacheManager::class)->getCache('hash')
708
        );
709
        $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, null, $id, $rootLine);
710
        $tsConfig = $parser->parse($pageTs, $matcher, $site);
711
        $cacheHash = md5(json_encode($tsConfig));
712
713
        // Get User TSconfig overlay, if no backend user is logged-in, this needs to be checked as well
714
        if (static::getBackendUserAuthentication()) {
715
            $userTSconfig = static::getBackendUserAuthentication()->getTSConfig() ?? [];
716
        } else {
717
            $userTSconfig = [];
718
        }
719
720
        if (is_array($userTSconfig['page.'] ?? null)) {
721
            // Override page TSconfig with user TSconfig
722
            ArrayUtility::mergeRecursiveWithOverrule($tsConfig, $userTSconfig['page.']);
723
            $cacheHash .= '_user' . static::getBackendUserAuthentication()->user['uid'];
724
        }
725
726
        // Many pages end up with the same ts config. To reduce memory usage, the cache
727
        // entries are a linked list: One or more pids point to content hashes which then
728
        // contain the cached content.
729
        $cache->set('pagesTsConfigHashToContent' . $cacheHash, $tsConfig, ['pagesTsConfig']);
730
        $cache->set('pagesTsConfigIdToHash' . $id, $cacheHash, ['pagesTsConfig']);
731
732
        return $tsConfig;
733
    }
734
735
    /*******************************************
736
     *
737
     * Users / Groups related
738
     *
739
     *******************************************/
740
    /**
741
     * Returns an array with be_users records of all user NOT DELETED sorted by their username
742
     * Keys in the array is the be_users uid
743
     *
744
     * @param string $fields Optional $fields list (default: username,usergroup,usergroup_cached_list,uid) can be used to set the selected fields
745
     * @param string $where Optional $where clause (fx. "AND username='pete'") can be used to limit query
746
     * @return array
747
     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
748
     */
749
    public static function getUserNames($fields = 'username,usergroup,usergroup_cached_list,uid', $where = '')
750
    {
751
        return self::getRecordsSortedByTitle(
752
            GeneralUtility::trimExplode(',', $fields, true),
753
            'be_users',
754
            'username',
755
            'AND pid=0 ' . $where
756
        );
757
    }
758
759
    /**
760
     * Returns an array with be_groups records (title, uid) of all groups NOT DELETED sorted by their title
761
     *
762
     * @param string $fields Field list
763
     * @param string $where WHERE clause
764
     * @return array
765
     * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements
766
     */
767
    public static function getGroupNames($fields = 'title,uid', $where = '')
768
    {
769
        return self::getRecordsSortedByTitle(
770
            GeneralUtility::trimExplode(',', $fields, true),
771
            'be_groups',
772
            'title',
773
            'AND pid=0 ' . $where
774
        );
775
    }
776
777
    /**
778
     * Returns an array of all non-deleted records of a table sorted by a given title field.
779
     * The value of the title field will be replaced by the return value
780
     * of self::getRecordTitle() before the sorting is performed.
781
     *
782
     * @param array $fields Fields to select
783
     * @param string $table Table name
784
     * @param string $titleField Field that will contain the record title
785
     * @param string $where Additional where clause
786
     * @return array Array of sorted records
787
     */
788
    protected static function getRecordsSortedByTitle(array $fields, $table, $titleField, $where = '')
789
    {
790
        $fieldsIndex = array_flip($fields);
791
        // Make sure the titleField is amongst the fields when getting sorted
792
        $fieldsIndex[$titleField] = 1;
793
794
        $result = [];
795
796
        $queryBuilder = static::getQueryBuilderForTable($table);
797
        $queryBuilder->getRestrictions()
798
            ->removeAll()
799
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
800
801
        $res = $queryBuilder
802
            ->select('*')
803
            ->from($table)
804
            ->where(QueryHelper::stripLogicalOperatorPrefix($where))
805
            ->execute();
806
807
        while ($record = $res->fetch()) {
808
            // store the uid, because it might be unset if it's not among the requested $fields
809
            $recordId = $record['uid'];
810
            $record[$titleField] = self::getRecordTitle($table, $record);
811
812
            // include only the requested fields in the result
813
            $result[$recordId] = array_intersect_key($record, $fieldsIndex);
814
        }
815
816
        // sort records by $sortField. This is not done in the query because the title might have been overwritten by
817
        // self::getRecordTitle();
818
        return ArrayUtility::sortArraysByKey($result, $titleField);
819
    }
820
821
    /**
822
     * Returns the array $usernames with the names of all users NOT IN $groupArray changed to the uid (hides the usernames!).
823
     * If $excludeBlindedFlag is set, then these records are unset from the array $usernames
824
     * 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
825
     *
826
     * @param array $usernames User names
827
     * @param array $groupArray Group names
828
     * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
829
     * @return array User names, blinded
830
     * @internal
831
     */
832
    public static function blindUserNames($usernames, $groupArray, $excludeBlindedFlag = false)
833
    {
834
        if (is_array($usernames) && is_array($groupArray)) {
0 ignored issues
show
introduced by
The condition is_array($groupArray) is always true.
Loading history...
835
            foreach ($usernames as $uid => $row) {
836
                $userN = $uid;
837
                $set = 0;
838
                if ($row['uid'] != static::getBackendUserAuthentication()->user['uid']) {
839
                    foreach ($groupArray as $v) {
840
                        if ($v && GeneralUtility::inList($row['usergroup_cached_list'], $v)) {
841
                            $userN = $row['username'];
842
                            $set = 1;
843
                        }
844
                    }
845
                } else {
846
                    $userN = $row['username'];
847
                    $set = 1;
848
                }
849
                $usernames[$uid]['username'] = $userN;
850
                if ($excludeBlindedFlag && !$set) {
851
                    unset($usernames[$uid]);
852
                }
853
            }
854
        }
855
        return $usernames;
856
    }
857
858
    /**
859
     * Corresponds to blindUserNames but works for groups instead
860
     *
861
     * @param array $groups Group names
862
     * @param array $groupArray Group names (reference)
863
     * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
864
     * @return array
865
     * @internal
866
     */
867
    public static function blindGroupNames($groups, $groupArray, $excludeBlindedFlag = false)
868
    {
869
        if (is_array($groups) && is_array($groupArray)) {
0 ignored issues
show
introduced by
The condition is_array($groupArray) is always true.
Loading history...
870
            foreach ($groups as $uid => $row) {
871
                $groupN = $uid;
872
                $set = 0;
873
                if (in_array($uid, $groupArray, false)) {
874
                    $groupN = $row['title'];
875
                    $set = 1;
876
                }
877
                $groups[$uid]['title'] = $groupN;
878
                if ($excludeBlindedFlag && !$set) {
879
                    unset($groups[$uid]);
880
                }
881
            }
882
        }
883
        return $groups;
884
    }
885
886
    /*******************************************
887
     *
888
     * Output related
889
     *
890
     *******************************************/
891
    /**
892
     * Returns the difference in days between input $tstamp and $EXEC_TIME
893
     *
894
     * @param int $tstamp Time stamp, seconds
895
     * @return int
896
     */
897
    public static function daysUntil($tstamp)
898
    {
899
        $delta_t = $tstamp - $GLOBALS['EXEC_TIME'];
900
        return ceil($delta_t / (3600 * 24));
901
    }
902
903
    /**
904
     * Returns $tstamp formatted as "ddmmyy" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'])
905
     *
906
     * @param int $tstamp Time stamp, seconds
907
     * @return string Formatted time
908
     */
909
    public static function date($tstamp)
910
    {
911
        return date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$tstamp);
912
    }
913
914
    /**
915
     * Returns $tstamp formatted as "ddmmyy hhmm" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] AND $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'])
916
     *
917
     * @param int $value Time stamp, seconds
918
     * @return string Formatted time
919
     */
920
    public static function datetime($value)
921
    {
922
        return date(
923
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
924
            $value
925
        );
926
    }
927
928
    /**
929
     * Returns $value (in seconds) formatted as hh:mm:ss
930
     * For instance $value = 3600 + 60*2 + 3 should return "01:02:03"
931
     *
932
     * @param int $value Time stamp, seconds
933
     * @param bool $withSeconds Output hh:mm:ss. If FALSE: hh:mm
934
     * @return string Formatted time
935
     */
936
    public static function time($value, $withSeconds = true)
937
    {
938
        return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value);
939
    }
940
941
    /**
942
     * Returns the "age" in minutes / hours / days / years of the number of $seconds inputted.
943
     *
944
     * @param int $seconds Seconds could be the difference of a certain timestamp and time()
945
     * @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")
946
     * @return string Formatted time
947
     */
948
    public static function calcAge($seconds, $labels = 'min|hrs|days|yrs|min|hour|day|year')
949
    {
950
        $labelArr = GeneralUtility::trimExplode('|', $labels, true);
951
        $absSeconds = abs($seconds);
952
        $sign = $seconds < 0 ? -1 : 1;
953
        if ($absSeconds < 3600) {
954
            $val = round($absSeconds / 60);
955
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[4] : $labelArr[0]);
956
        } elseif ($absSeconds < 24 * 3600) {
957
            $val = round($absSeconds / 3600);
958
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[5] : $labelArr[1]);
959
        } elseif ($absSeconds < 365 * 24 * 3600) {
960
            $val = round($absSeconds / (24 * 3600));
961
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[6] : $labelArr[2]);
962
        } else {
963
            $val = round($absSeconds / (365 * 24 * 3600));
964
            $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[7] : $labelArr[3]);
965
        }
966
        return $seconds;
967
    }
968
969
    /**
970
     * Returns a formatted timestamp if $tstamp is set.
971
     * The date/datetime will be followed by the age in parenthesis.
972
     *
973
     * @param int $tstamp Time stamp, seconds
974
     * @param int $prefix 1/-1 depending on polarity of age.
975
     * @param string $date $date=="date" will yield "dd:mm:yy" formatting, otherwise "dd:mm:yy hh:mm
976
     * @return string
977
     */
978
    public static function dateTimeAge($tstamp, $prefix = 1, $date = '')
979
    {
980
        if (!$tstamp) {
981
            return '';
982
        }
983
        $label = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
984
        $age = ' (' . self::calcAge($prefix * ($GLOBALS['EXEC_TIME'] - $tstamp), $label) . ')';
985
        return ($date === 'date' ? self::date($tstamp) : self::datetime($tstamp)) . $age;
986
    }
987
988
    /**
989
     * Resolves file references for a given record.
990
     *
991
     * @param string $tableName Name of the table of the record
992
     * @param string $fieldName Name of the field of the record
993
     * @param array $element Record data
994
     * @param int|null $workspaceId Workspace to fetch data for
995
     * @return \TYPO3\CMS\Core\Resource\FileReference[]|null
996
     */
997
    public static function resolveFileReferences($tableName, $fieldName, $element, $workspaceId = null)
998
    {
999
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1000
            return null;
1001
        }
1002
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1003
        if (empty($configuration['type']) || $configuration['type'] !== 'inline'
1004
            || empty($configuration['foreign_table']) || $configuration['foreign_table'] !== 'sys_file_reference'
1005
        ) {
1006
            return null;
1007
        }
1008
1009
        $fileReferences = [];
1010
        /** @var RelationHandler $relationHandler */
1011
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1012
        if ($workspaceId !== null) {
1013
            $relationHandler->setWorkspaceId($workspaceId);
1014
        }
1015
        $relationHandler->start(
1016
            $element[$fieldName],
1017
            $configuration['foreign_table'],
1018
            $configuration['MM'] ?? '',
1019
            $element['uid'],
1020
            $tableName,
1021
            $configuration
1022
        );
1023
        $relationHandler->processDeletePlaceholder();
1024
        $referenceUids = $relationHandler->tableArray[$configuration['foreign_table']];
1025
1026
        foreach ($referenceUids as $referenceUid) {
1027
            try {
1028
                $fileReference = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject(
1029
                    $referenceUid,
1030
                    [],
1031
                    $workspaceId === 0
1032
                );
1033
                $fileReferences[$fileReference->getUid()] = $fileReference;
1034
            } catch (FileDoesNotExistException $e) {
1035
                /**
1036
                 * We just catch the exception here
1037
                 * Reasoning: There is nothing an editor or even admin could do
1038
                 */
1039
            } catch (\InvalidArgumentException $e) {
1040
                /**
1041
                 * The storage does not exist anymore
1042
                 * Log the exception message for admins as they maybe can restore the storage
1043
                 */
1044
                self::getLogger()->error($e->getMessage(), ['table' => $tableName, 'fieldName' => $fieldName, 'referenceUid' => $referenceUid, 'exception' => $e]);
1045
            }
1046
        }
1047
1048
        return $fileReferences;
1049
    }
1050
1051
    /**
1052
     * Returns a linked image-tag for thumbnail(s)/fileicons/truetype-font-previews from a database row with sys_file_references
1053
     * All $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] extension are made to thumbnails + ttf file (renders font-example)
1054
     * Thumbnails are linked to ShowItemController (/thumbnails route)
1055
     *
1056
     * @param array $row Row is the database row from the table, $table.
1057
     * @param string $table Table name for $row (present in TCA)
1058
     * @param string $field Field is pointing to the connecting field of sys_file_references
1059
     * @param string $backPath Back path prefix for image tag src="" field
1060
     * @param string $thumbScript UNUSED since FAL
1061
     * @param string $uploaddir UNUSED since FAL
1062
     * @param int $abs UNUSED
1063
     * @param string $tparams Optional: $tparams is additional attributes for the image tags
1064
     * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default.
1065
     * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup
1066
     * @return string Thumbnail image tag.
1067
     */
1068
    public static function thumbCode(
1069
        $row,
1070
        $table,
1071
        $field,
1072
        $backPath = '',
1073
        $thumbScript = '',
1074
        $uploaddir = null,
1075
        $abs = 0,
1076
        $tparams = '',
1077
        $size = '',
1078
        $linkInfoPopup = true
1079
    ) {
1080
        // Check and parse the size parameter
1081
        $size = trim($size);
1082
        $sizeParts = [64, 64];
1083
        if ($size) {
1084
            $sizeParts = explode('x', $size . 'x' . $size);
1085
        }
1086
        $thumbData = '';
1087
        $fileReferences = static::resolveFileReferences($table, $field, $row);
1088
        // FAL references
1089
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1090
        if ($fileReferences !== null) {
1091
            foreach ($fileReferences as $fileReferenceObject) {
1092
                // Do not show previews of hidden references
1093
                if ($fileReferenceObject->getProperty('hidden')) {
1094
                    continue;
1095
                }
1096
                $fileObject = $fileReferenceObject->getOriginalFile();
1097
1098
                if ($fileObject->isMissing()) {
1099
                    $thumbData .= '<span class="label label-danger">'
1100
                        . htmlspecialchars(
1101
                            static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')
1102
                        )
1103
                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1104
                    continue;
1105
                }
1106
1107
                // Preview web image or media elements
1108
                if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
1109
                    && GeneralUtility::inList(
1110
                        $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
1111
                        $fileReferenceObject->getExtension()
1112
                    )
1113
                ) {
1114
                    $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
1115
                    $cropArea = $cropVariantCollection->getCropArea();
1116
                    $imageUrl = self::getThumbnailUrl($fileObject->getUid(), [
1117
                        'width' => $sizeParts[0],
1118
                        'height' => $sizeParts[1] . 'c',
1119
                        'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
1120
                        '_context' => $cropArea->isEmpty() ? ProcessedFile::CONTEXT_IMAGEPREVIEW : ProcessedFile::CONTEXT_IMAGECROPSCALEMASK
1121
                    ]);
1122
                    $attributes = [
1123
                        'src' => $imageUrl,
1124
                        'width' => (int)$sizeParts[0],
1125
                        'height' => (int)$sizeParts[1],
1126
                        'alt' => $fileReferenceObject->getName(),
1127
                    ];
1128
                    $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . $tparams . '/>';
1129
                } else {
1130
                    // Icon
1131
                    $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1132
                        . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1133
                        . '</span>';
1134
                }
1135
                if ($linkInfoPopup) {
1136
                    // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
1137
                    $attributes = GeneralUtility::implodeAttributes([
1138
                        'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
1139
                        'data-dispatch-args-list' => '_FILE,' . (int)$fileObject->getUid(),
1140
                    ], true);
1141
                    $thumbData .= '<a href="#" ' . $attributes . '>' . $imgTag . '</a> ';
1142
                } else {
1143
                    $thumbData .= $imgTag;
1144
                }
1145
            }
1146
        }
1147
        return $thumbData;
1148
    }
1149
1150
    /**
1151
     * @param int $fileId
1152
     * @param array $configuration
1153
     * @return string
1154
     * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
1155
     */
1156
    public static function getThumbnailUrl(int $fileId, array $configuration): string
1157
    {
1158
        $parameters = json_encode([
1159
            'fileId' => $fileId,
1160
            'configuration' => $configuration
1161
        ]);
1162
        $uriParameters = [
1163
            'parameters' => $parameters,
1164
            'hmac' => GeneralUtility::hmac(
1165
                $parameters,
1166
                ThumbnailController::class
1167
            ),
1168
        ];
1169
        return (string)GeneralUtility::makeInstance(UriBuilder::class)
1170
            ->buildUriFromRoute('thumbnails', $uriParameters);
1171
    }
1172
1173
    /**
1174
     * Returns title-attribute information for a page-record informing about id, doktype, hidden, starttime, endtime, fe_group etc.
1175
     *
1176
     * @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)
1177
     * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4)
1178
     * @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
1179
     * @return string
1180
     */
1181
    public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true)
1182
    {
1183
        $lang = static::getLanguageService();
1184
        $parts = [];
1185
        $parts[] = 'id=' . $row['uid'];
1186
        if ($row['uid'] === 0) {
1187
            $out = htmlspecialchars($parts[0]);
1188
            return $includeAttrib ? 'title="' . $out . '"' : $out;
1189
        }
1190
        switch (VersionState::cast($row['t3ver_state'])) {
1191
            case new VersionState(VersionState::NEW_PLACEHOLDER):
1192
                $parts[] = 'PLH WSID#' . $row['t3ver_wsid'];
1193
                break;
1194
            case new VersionState(VersionState::DELETE_PLACEHOLDER):
1195
                $parts[] = 'Deleted element!';
1196
                break;
1197
            case new VersionState(VersionState::MOVE_POINTER):
1198
                $parts[] = 'NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid'];
1199
                break;
1200
            case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1201
                $parts[] = 'New element!';
1202
                break;
1203
        }
1204
        if ($row['doktype'] == PageRepository::DOKTYPE_LINK) {
1205
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label']) . ' ' . $row['url'];
1206
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1207
            if ($perms_clause) {
1208
                $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20);
1209
            } else {
1210
                $row['shortcut'] = (int)$row['shortcut'];
1211
                $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title');
1212
                $label = $lRec['title'] . ' (id=' . $row['shortcut'] . ')';
1213
            }
1214
            if ($row['shortcut_mode'] != PageRepository::SHORTCUT_MODE_NONE) {
1215
                $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' '
1216
                    . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode']));
1217
            }
1218
            $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

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

1882
                            $relationRecord = static::getRecordWSOL($relationTableNameForField, /** @scrutinizer ignore-type */ $explodedValue);
Loading history...
1883
                            $finalValues[] = static::getRecordTitle($relationTableNameForField, $relationRecord);
1884
                        }
1885
                        $l = implode(', ', $finalValues);
1886
                    }
1887
                } else {
1888
                    $l = implode(', ', GeneralUtility::trimExplode(',', $value, true));
1889
                }
1890
                break;
1891
            case 'check':
1892
                if (!is_array($theColConf['items'])) {
1893
                    $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');
1894
                } elseif (count($theColConf['items']) === 1) {
1895
                    reset($theColConf['items']);
1896
                    $invertStateDisplay = current($theColConf['items'])['invertStateDisplay'] ?? false;
1897
                    if ($invertStateDisplay) {
1898
                        $value = !$value;
1899
                    }
1900
                    $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');
1901
                } else {
1902
                    $lA = [];
1903
                    foreach ($theColConf['items'] as $key => $val) {
1904
                        if ($value & 2 ** $key) {
1905
                            $lA[] = $lang->sL($val[0]);
1906
                        }
1907
                    }
1908
                    $l = implode(', ', $lA);
1909
                }
1910
                break;
1911
            case 'input':
1912
                // Hide value 0 for dates, but show it for everything else
1913
                // todo: phpstan states that $value always exists and is not nullable. At the moment, this is a false
1914
                //       positive as null can be passed into this method via $value. As soon as more strict types are
1915
                //       used, this isset check must be replaced with a more appropriate check.
1916
                if (isset($value)) {
1917
                    $dateTimeFormats = QueryHelper::getDateTimeFormats();
1918
1919
                    if (GeneralUtility::inList($theColConf['eval'] ?? '', 'date')) {
1920
                        // Handle native date field
1921
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') {
1922
                            $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value);
1923
                        } else {
1924
                            $value = (int)$value;
1925
                        }
1926
                        if (!empty($value)) {
1927
                            $ageSuffix = '';
1928
                            $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
1929
                            $ageDisplayKey = 'disableAgeDisplay';
1930
1931
                            // generate age suffix as long as not explicitly suppressed
1932
                            if (!isset($dateColumnConfiguration[$ageDisplayKey])
1933
                                // non typesafe comparison on intention
1934
                                || $dateColumnConfiguration[$ageDisplayKey] == false
1935
                            ) {
1936
                                $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '')
1937
                                    . self::calcAge(
1938
                                        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

1938
                                        /** @scrutinizer ignore-type */ abs($GLOBALS['EXEC_TIME'] - $value),
Loading history...
1939
                                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
1940
                                    )
1941
                                    . ')';
1942
                            }
1943
1944
                            $l = self::date($value) . $ageSuffix;
1945
                        }
1946
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'time')) {
1947
                        // Handle native time field
1948
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
1949
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
1950
                        } else {
1951
                            $value = (int)$value;
1952
                        }
1953
                        if (!empty($value)) {
1954
                            $l = gmdate('H:i', (int)$value);
1955
                        }
1956
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'timesec')) {
1957
                        // Handle native time field
1958
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
1959
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
1960
                        } else {
1961
                            $value = (int)$value;
1962
                        }
1963
                        if (!empty($value)) {
1964
                            $l = gmdate('H:i:s', (int)$value);
1965
                        }
1966
                    } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'datetime')) {
1967
                        // Handle native datetime field
1968
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') {
1969
                            $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value);
1970
                        } else {
1971
                            $value = (int)$value;
1972
                        }
1973
                        if (!empty($value)) {
1974
                            $l = self::datetime($value);
1975
                        }
1976
                    } else {
1977
                        $l = $value;
1978
                    }
1979
                }
1980
                break;
1981
            case 'flex':
1982
                $l = strip_tags($value);
1983
                break;
1984
            default:
1985
                if ($defaultPassthrough) {
1986
                    $l = $value;
1987
                } elseif (isset($theColConf['MM'])) {
1988
                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1989
                } elseif ($value) {
1990
                    $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200);
1991
                }
1992
        }
1993
        // If this field is a password field, then hide the password by changing it to a random number of asterisk (*)
1994
        if (!empty($theColConf['eval']) && stripos($theColConf['eval'], 'password') !== false) {
1995
            $l = '';
1996
            $randomNumber = random_int(5, 12);
1997
            for ($i = 0; $i < $randomNumber; $i++) {
1998
                $l .= '*';
1999
            }
2000
        }
2001
        /*****************
2002
         *HOOK: post-processing the human readable output from a record
2003
         ****************/
2004
        $null = null;
2005
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) {
2006
            $params = [
2007
                'value' => $l,
2008
                'colConf' => $theColConf
2009
            ];
2010
            $l = GeneralUtility::callUserFunction($_funcRef, $params, $null);
2011
        }
2012
        if ($fixed_lgd_chars) {
2013
            return GeneralUtility::fixed_lgd_cs($l, $fixed_lgd_chars);
2014
        }
2015
        return $l;
2016
    }
2017
2018
    /**
2019
     * 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.
2020
     *
2021
     * @param string $table Table name, present in TCA
2022
     * @param string $fN Field name
2023
     * @param string $fV Field value
2024
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
2025
     * @param int $uid Uid of the current record
2026
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
2027
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
2028
     * @return string
2029
     * @see getProcessedValue()
2030
     */
2031
    public static function getProcessedValueExtra(
2032
        $table,
2033
        $fN,
2034
        $fV,
2035
        $fixed_lgd_chars = 0,
2036
        $uid = 0,
2037
        $forceResult = true,
2038
        $pid = 0
2039
    ) {
2040
        $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, 1, 0, $uid, $forceResult, $pid);
2041
        if (!isset($fVnew)) {
2042
            if (is_array($GLOBALS['TCA'][$table])) {
2043
                if ($fN == $GLOBALS['TCA'][$table]['ctrl']['tstamp'] || $fN == $GLOBALS['TCA'][$table]['ctrl']['crdate']) {
2044
                    $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

2044
                    $fVnew = self::datetime(/** @scrutinizer ignore-type */ $fV);
Loading history...
2045
                } elseif ($fN === 'pid') {
2046
                    // Fetches the path with no regard to the users permissions to select pages.
2047
                    $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

2047
                    $fVnew = self::getRecordPath(/** @scrutinizer ignore-type */ $fV, '1=1', 20);
Loading history...
2048
                } else {
2049
                    $fVnew = $fV;
2050
                }
2051
            }
2052
        }
2053
        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...
2054
    }
2055
2056
    /**
2057
     * Returns fields for a table, $table, which would typically be interesting to select
2058
     * This includes uid, the fields defined for title, icon-field.
2059
     * 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)
2060
     *
2061
     * @param string $table Table name, present in $GLOBALS['TCA']
2062
     * @param string $prefix Table prefix
2063
     * @param array $fields Preset fields (must include prefix if that is used)
2064
     * @return string List of fields.
2065
     * @internal should only be used from within TYPO3 Core
2066
     */
2067
    public static function getCommonSelectFields($table, $prefix = '', $fields = [])
2068
    {
2069
        $fields[] = $prefix . 'uid';
2070
        if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
2071
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
2072
        }
2073
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) {
2074
            $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
2075
            foreach ($secondFields as $fieldN) {
2076
                $fields[] = $prefix . $fieldN;
2077
            }
2078
        }
2079
        if (static::isTableWorkspaceEnabled($table)) {
2080
            $fields[] = $prefix . 't3ver_state';
2081
            $fields[] = $prefix . 't3ver_wsid';
2082
        }
2083
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) {
2084
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
2085
        }
2086
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
2087
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
2088
        }
2089
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
2090
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2091
        }
2092
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) {
2093
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
2094
        }
2095
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) {
2096
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
2097
        }
2098
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
2099
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
2100
        }
2101
        return implode(',', array_unique($fields));
2102
    }
2103
2104
    /*******************************************
2105
     *
2106
     * Backend Modules API functions
2107
     *
2108
     *******************************************/
2109
2110
    /**
2111
     * Returns CSH help text (description), if configured for, as an array (title, description)
2112
     *
2113
     * @param string $table Table name
2114
     * @param string $field Field name
2115
     * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
2116
     * @internal should only be used from within TYPO3 Core
2117
     */
2118
    public static function helpTextArray($table, $field)
2119
    {
2120
        if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
2121
            static::getLanguageService()->loadSingleTableDescription($table);
2122
        }
2123
        $output = [
2124
            'description' => null,
2125
            'title' => null,
2126
            'moreInfo' => false
2127
        ];
2128
        if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
2129
            $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
2130
            // Add alternative title, if defined
2131
            if ($data['alttitle']) {
2132
                $output['title'] = $data['alttitle'];
2133
            }
2134
            // If we have more information to show and access to the cshmanual
2135
            if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax'])
2136
                && static::getBackendUserAuthentication()->check('modules', 'help_CshmanualCshmanual')
2137
            ) {
2138
                $output['moreInfo'] = true;
2139
            }
2140
            // Add description
2141
            if ($data['description']) {
2142
                $output['description'] = $data['description'];
2143
            }
2144
        }
2145
        return $output;
2146
    }
2147
2148
    /**
2149
     * Returns CSH help text
2150
     *
2151
     * @param string $table Table name
2152
     * @param string $field Field name
2153
     * @return string HTML content for help text
2154
     * @see cshItem()
2155
     * @internal should only be used from within TYPO3 Core
2156
     */
2157
    public static function helpText($table, $field)
2158
    {
2159
        $helpTextArray = self::helpTextArray($table, $field);
2160
        $output = '';
2161
        $arrow = '';
2162
        // Put header before the rest of the text
2163
        if ($helpTextArray['title'] !== null) {
2164
            $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
2165
        }
2166
        // Add see also arrow if we have more info
2167
        if ($helpTextArray['moreInfo']) {
2168
            /** @var IconFactory $iconFactory */
2169
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2170
            $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
2171
        }
2172
        // Wrap description and arrow in p tag
2173
        if ($helpTextArray['description'] !== null || $arrow) {
2174
            $output .= '<p class="help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>';
2175
        }
2176
        return $output;
2177
    }
2178
2179
    /**
2180
     * API function that wraps the text / html in help text, so if a user hovers over it
2181
     * the help text will show up
2182
     *
2183
     * @param string $table The table name for which the help should be shown
2184
     * @param string $field The field name for which the help should be shown
2185
     * @param string $text The text which should be wrapped with the help text
2186
     * @param array $overloadHelpText Array with text to overload help text
2187
     * @return string the HTML code ready to render
2188
     * @internal should only be used from within TYPO3 Core
2189
     */
2190
    public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
2191
    {
2192
        // Initialize some variables
2193
        $helpText = '';
2194
        $abbrClassAdd = '';
2195
        $hasHelpTextOverload = !empty($overloadHelpText);
2196
        // Get the help text that should be shown on hover
2197
        if (!$hasHelpTextOverload) {
2198
            $helpText = self::helpText($table, $field);
2199
        }
2200
        // If there's a help text or some overload information, proceed with preparing an output
2201
        if (!empty($helpText) || $hasHelpTextOverload) {
2202
            // If no text was given, just use the regular help icon
2203
            if ($text == '') {
2204
                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2205
                $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2206
                $abbrClassAdd = ' help-teaser-icon';
2207
            }
2208
            $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2209
            $wrappedText = '<span class="help-link" href="#" data-table="' . $table . '" data-field="' . $field . '"';
2210
            // The overload array may provide a title and a description
2211
            // If either one is defined, add them to the "data" attributes
2212
            if ($hasHelpTextOverload) {
2213
                if (isset($overloadHelpText['title'])) {
2214
                    $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2215
                }
2216
                if (isset($overloadHelpText['description'])) {
2217
                    $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2218
                }
2219
            }
2220
            $wrappedText .= '>' . $text . '</span>';
2221
            return $wrappedText;
2222
        }
2223
        return $text;
2224
    }
2225
2226
    /**
2227
     * API for getting CSH icons/text for use in backend modules.
2228
     * TCA_DESCR will be loaded if it isn't already
2229
     *
2230
     * @param string $table Table name ('_MOD_'+module name)
2231
     * @param string $field Field name (CSH locallang main key)
2232
     * @param string $_ (unused)
2233
     * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2234
     * @return string HTML content for help text
2235
     */
2236
    public static function cshItem($table, $field, $_ = '', $wrap = '')
2237
    {
2238
        static::getLanguageService()->loadSingleTableDescription($table);
2239
        if (is_array($GLOBALS['TCA_DESCR'][$table])
2240
            && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])
2241
        ) {
2242
            // Creating short description
2243
            $output = self::wrapInHelp($table, $field);
2244
            if ($output && $wrap) {
2245
                $wrParts = explode('|', $wrap);
2246
                $output = $wrParts[0] . $output . $wrParts[1];
2247
            }
2248
            return $output;
2249
        }
2250
        return '';
2251
    }
2252
2253
    /**
2254
     * Returns a JavaScript string for viewing the page id, $id
2255
     * It will re-use any window already open.
2256
     *
2257
     * @param int $pageUid Page UID
2258
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2259
     * @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)
2260
     * @param string $anchorSection Optional anchor to the URL
2261
     * @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!
2262
     * @param string $additionalGetVars Additional GET variables.
2263
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2264
     * @return string
2265
     */
2266
    public static function viewOnClick(
2267
        $pageUid,
2268
        $backPath = '',
2269
        $rootLine = null,
2270
        $anchorSection = '',
2271
        $alternativeUrl = '',
2272
        $additionalGetVars = '',
2273
        $switchFocus = true
2274
    ) {
2275
        try {
2276
            $previewUrl = self::getPreviewUrl(
2277
                $pageUid,
2278
                $backPath,
2279
                $rootLine,
2280
                $anchorSection,
2281
                $alternativeUrl,
2282
                $additionalGetVars,
2283
                $switchFocus
2284
            );
2285
        } catch (UnableToLinkToPageException $e) {
2286
            return '';
2287
        }
2288
2289
        $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');'
2290
            . ($switchFocus ? 'previewWin.focus();' : '') . LF
2291
            . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };';
2292
2293
        return $onclickCode;
2294
    }
2295
2296
    /**
2297
     * Returns the preview url
2298
     *
2299
     * It will detect the correct domain name if needed and provide the link with the right back path.
2300
     *
2301
     * @param int $pageUid Page UID
2302
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2303
     * @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)
2304
     * @param string $anchorSection Optional anchor to the URL
2305
     * @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!
2306
     * @param string $additionalGetVars Additional GET variables.
2307
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2308
     * @return string
2309
     */
2310
    public static function getPreviewUrl(
2311
        $pageUid,
2312
        $backPath = '',
2313
        $rootLine = null,
2314
        $anchorSection = '',
2315
        $alternativeUrl = '',
2316
        $additionalGetVars = '',
2317
        &$switchFocus = true
2318
    ): string {
2319
        $viewScript = '/index.php?id=';
2320
        if ($alternativeUrl) {
2321
            $viewScript = $alternativeUrl;
2322
        }
2323
2324
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2325
            $hookObj = GeneralUtility::makeInstance($className);
2326
            if (method_exists($hookObj, 'preProcess')) {
2327
                $hookObj->preProcess(
2328
                    $pageUid,
2329
                    $backPath,
2330
                    $rootLine,
2331
                    $anchorSection,
2332
                    $viewScript,
2333
                    $additionalGetVars,
2334
                    $switchFocus
2335
                );
2336
            }
2337
        }
2338
2339
        // If there is an alternative URL or the URL has been modified by a hook, use that one.
2340
        if ($alternativeUrl || $viewScript !== '/index.php?id=') {
2341
            $previewUrl = $viewScript;
2342
        } else {
2343
            $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2344
            $pageInfo = self::readPageAccess($pageUid, $permissionClause);
2345
            // prepare custom context for link generation (to allow for example time based previews)
2346
            $context = clone GeneralUtility::makeInstance(Context::class);
2347
            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context);
2348
2349
            // Build the URL with a site as prefix, if configured
2350
            $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
2351
            // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is
2352
            $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid);
2353
            try {
2354
                $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine);
2355
            } catch (SiteNotFoundException $e) {
2356
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794919);
2357
            }
2358
            // Create a multi-dimensional array out of the additional get vars
2359
            $additionalQueryParams = [];
2360
            parse_str($additionalGetVars, $additionalQueryParams);
2361
            if (isset($additionalQueryParams['L'])) {
2362
                $additionalQueryParams['_language'] = $additionalQueryParams['_language'] ?? $additionalQueryParams['L'];
2363
                unset($additionalQueryParams['L']);
2364
            }
2365
            try {
2366
                $previewUrl = (string)$site->getRouter($context)->generateUri(
2367
                    $pageUid,
2368
                    $additionalQueryParams,
2369
                    $anchorSection,
2370
                    RouterInterface::ABSOLUTE_URL
2371
                );
2372
            } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
2373
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794914);
2374
            }
2375
        }
2376
2377
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2378
            $hookObj = GeneralUtility::makeInstance($className);
2379
            if (method_exists($hookObj, 'postProcess')) {
2380
                $previewUrl = $hookObj->postProcess(
2381
                    $previewUrl,
2382
                    $pageUid,
2383
                    $rootLine,
2384
                    $anchorSection,
2385
                    $viewScript,
2386
                    $additionalGetVars,
2387
                    $switchFocus
2388
                );
2389
            }
2390
        }
2391
2392
        return $previewUrl;
2393
    }
2394
2395
    /**
2396
     * Makes click menu link (context sensitive menu)
2397
     *
2398
     * Returns $str wrapped in a link which will activate the context sensitive
2399
     * menu for the record ($table/$uid) or file ($table = file)
2400
     * The link will load the top frame with the parameter "&item" which is the table, uid
2401
     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2402
     *
2403
     * @param string $content String to be wrapped in link, typ. image tag.
2404
     * @param string $table Table name/File path. If the icon is for a database
2405
     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2406
     * the absolute filepath
2407
     * @param int|string $uid If icon is for database record this is the UID for the
2408
     * record from $table or identifier for sys_file record
2409
     * @param string $context Set tree if menu is called from tree view
2410
     * @param string $_addParams NOT IN USE
2411
     * @param string $_enDisItems NOT IN USE
2412
     * @param bool $returnTagParameters If set, will return only the onclick
2413
     * JavaScript, not the whole link.
2414
     *
2415
     * @return string The link wrapped input string.
2416
     */
2417
    public static function wrapClickMenuOnIcon(
2418
        $content,
2419
        $table,
2420
        $uid = 0,
2421
        $context = '',
2422
        $_addParams = '',
2423
        $_enDisItems = '',
2424
        $returnTagParameters = false
2425
    ) {
2426
        $tagParameters = [
2427
            'class' => 't3js-contextmenutrigger',
2428
            'data-table' => $table,
2429
            'data-uid' => $uid,
2430
            'data-context' => $context
2431
        ];
2432
2433
        if ($returnTagParameters) {
2434
            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...
2435
        }
2436
        return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2437
    }
2438
2439
    /**
2440
     * Returns a URL with a command to TYPO3 Datahandler
2441
     *
2442
     * @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
2443
     * @param string $redirectUrl Redirect URL, default is to use GeneralUtility::getIndpEnv('REQUEST_URI')
2444
     * @return string
2445
     */
2446
    public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2447
    {
2448
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2449
        $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2450
        $url .= rawurlencode($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI'));
2451
        return $url;
2452
    }
2453
2454
    /**
2455
     * Returns a selector box "function menu" for a module
2456
     * See Inside TYPO3 for details about how to use / make Function menus
2457
     *
2458
     * @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=...
2459
     * @param string $elementName The form elements name, probably something like "SET[...]
2460
     * @param string $currentValue The value to be selected currently.
2461
     * @param array $menuItems An array with the menu items for the selector box
2462
     * @param string $script The script to send the &id to, if empty it's automatically found
2463
     * @param string $addParams Additional parameters to pass to the script.
2464
     * @return string HTML code for selector box
2465
     */
2466
    public static function getFuncMenu(
2467
        $mainParams,
2468
        $elementName,
2469
        $currentValue,
2470
        $menuItems,
2471
        $script = '',
2472
        $addParams = ''
2473
    ) {
2474
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2475
            return '';
2476
        }
2477
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2478
        $options = [];
2479
        foreach ($menuItems as $value => $label) {
2480
            $options[] = '<option value="'
2481
                . htmlspecialchars($value) . '"'
2482
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2483
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2484
        }
2485
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2486
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2487
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2488
        if (!empty($options)) {
2489
            // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2490
            $attributes = GeneralUtility::implodeAttributes([
2491
                'name' => $elementName,
2492
                'class' => 'form-control',
2493
                'data-menu-identifier' => $dataMenuIdentifier,
2494
                'data-global-event' => 'change',
2495
                'data-action-navigate' => '$data=~s/$value/',
2496
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2497
            ], true);
2498
            return sprintf(
2499
                '<select %s>%s</select>',
2500
                $attributes,
2501
                implode('', $options)
2502
            );
2503
        }
2504
        return '';
2505
    }
2506
2507
    /**
2508
     * Returns a selector box to switch the view
2509
     * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose.
2510
     * Mingling with getFuncMenu would harm the docHeader Menu.
2511
     *
2512
     * @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=...
2513
     * @param string $elementName The form elements name, probably something like "SET[...]
2514
     * @param string $currentValue The value to be selected currently.
2515
     * @param array $menuItems An array with the menu items for the selector box
2516
     * @param string $script The script to send the &id to, if empty it's automatically found
2517
     * @param string $addParams Additional parameters to pass to the script.
2518
     * @return string HTML code for selector box
2519
     */
2520
    public static function getDropdownMenu(
2521
        $mainParams,
2522
        $elementName,
2523
        $currentValue,
2524
        $menuItems,
2525
        $script = '',
2526
        $addParams = ''
2527
    ) {
2528
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2529
            return '';
2530
        }
2531
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2532
        $options = [];
2533
        foreach ($menuItems as $value => $label) {
2534
            $options[] = '<option value="'
2535
                . htmlspecialchars($value) . '"'
2536
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2537
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2538
        }
2539
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2540
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2541
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2542
        if (!empty($options)) {
2543
            // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2544
            $attributes = GeneralUtility::implodeAttributes([
2545
                'name' => $elementName,
2546
                'data-menu-identifier' => $dataMenuIdentifier,
2547
                'data-global-event' => 'change',
2548
                'data-action-navigate' => '$data=~s/$value/',
2549
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2550
            ], true);
2551
            return '
2552
			<div class="form-group">
2553
				<!-- Function Menu of module -->
2554
				<select class="form-control input-sm" ' . $attributes . '>
2555
					' . implode(LF, $options) . '
2556
				</select>
2557
			</div>
2558
						';
2559
        }
2560
        return '';
2561
    }
2562
2563
    /**
2564
     * Checkbox function menu.
2565
     * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox.
2566
     *
2567
     * @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=...
2568
     * @param string $elementName The form elements name, probably something like "SET[...]
2569
     * @param string $currentValue The value to be selected currently.
2570
     * @param string $script The script to send the &id to, if empty it's automatically found
2571
     * @param string $addParams Additional parameters to pass to the script.
2572
     * @param string $tagParams Additional attributes for the checkbox input tag
2573
     * @return string HTML code for checkbox
2574
     * @see getFuncMenu()
2575
     */
2576
    public static function getFuncCheck(
2577
        $mainParams,
2578
        $elementName,
2579
        $currentValue,
2580
        $script = '',
2581
        $addParams = '',
2582
        $tagParams = ''
2583
    ) {
2584
        // relies on module 'TYPO3/CMS/Backend/ActionDispatcher'
2585
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2586
        $attributes = GeneralUtility::implodeAttributes([
2587
            'type' => 'checkbox',
2588
            'class' => 'checkbox',
2589
            'name' => $elementName,
2590
            'value' => 1,
2591
            'data-global-event' => 'change',
2592
            'data-action-navigate' => '$data=~s/$value/',
2593
            'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName),
2594
            'data-empty-value' => '0',
2595
        ], true);
2596
        return
2597
            '<input ' . $attributes .
2598
            ($currentValue ? ' checked="checked"' : '') .
2599
            ($tagParams ? ' ' . $tagParams : '') .
2600
            ' />';
2601
    }
2602
2603
    /**
2604
     * Input field function menu
2605
     * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange"
2606
     *
2607
     * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
2608
     * @param string $elementName The form elements name, probably something like "SET[...]
2609
     * @param string $currentValue The value to be selected currently.
2610
     * @param int $size Relative size of input field, max is 48
2611
     * @param string $script The script to send the &id to, if empty it's automatically found
2612
     * @param string $addParams Additional parameters to pass to the script.
2613
     * @return string HTML code for input text field.
2614
     * @see getFuncMenu()
2615
     */
2616
    public static function getFuncInput(
2617
        $mainParams,
2618
        $elementName,
2619
        $currentValue,
2620
        $size = 10,
2621
        $script = '',
2622
        $addParams = ''
2623
    ) {
2624
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2625
        $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value);';
2626
        return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />';
2627
    }
2628
2629
    /**
2630
     * Builds the URL to the current script with given arguments
2631
     *
2632
     * @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=...
2633
     * @param string $addParams Additional parameters to pass to the script.
2634
     * @param string $script The script to send the &id to, if empty it's automatically found
2635
     * @return string The complete script URL
2636
     */
2637
    protected static function buildScriptUrl($mainParams, $addParams, $script = '')
2638
    {
2639
        if (!is_array($mainParams)) {
2640
            $mainParams = ['id' => $mainParams];
2641
        }
2642
        if (!$script) {
2643
            $script = PathUtility::basename(Environment::getCurrentScript());
2644
        }
2645
2646
        if ($routePath = GeneralUtility::_GP('route')) {
2647
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2648
            $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
2649
            $scriptUrl .= $addParams;
2650
        } else {
2651
            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
2652
        }
2653
2654
        return $scriptUrl;
2655
    }
2656
2657
    /**
2658
     * Call to update the page tree frame (or something else..?) after
2659
     * use 'updatePageTree' as a first parameter will set the page tree to be updated.
2660
     *
2661
     * @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.
2662
     * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
2663
     * @see BackendUtility::getUpdateSignalCode()
2664
     */
2665
    public static function setUpdateSignal($set = '', $params = '')
2666
    {
2667
        $beUser = static::getBackendUserAuthentication();
2668
        $modData = $beUser->getModuleData(
2669
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
2670
            'ses'
2671
        );
2672
        if ($set) {
2673
            $modData[$set] = [
2674
                'set' => $set,
2675
                'parameter' => $params
2676
            ];
2677
        } else {
2678
            // clear the module data
2679
            $modData = [];
2680
        }
2681
        $beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData);
2682
    }
2683
2684
    /**
2685
     * Call to update the page tree frame (or something else..?) if this is set by the function
2686
     * setUpdateSignal(). It will return some JavaScript that does the update
2687
     *
2688
     * @return string HTML javascript code
2689
     * @see BackendUtility::setUpdateSignal()
2690
     */
2691
    public static function getUpdateSignalCode()
2692
    {
2693
        $signals = [];
2694
        $modData = static::getBackendUserAuthentication()->getModuleData(
2695
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
2696
            'ses'
2697
        );
2698
        if (empty($modData)) {
2699
            return '';
2700
        }
2701
        // Hook: Allows to let TYPO3 execute your JS code
2702
        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
2703
        // Loop through all setUpdateSignals and get the JS code
2704
        foreach ($modData as $set => $val) {
2705
            if (isset($updateSignals[$set])) {
2706
                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => ''];
2707
                $ref = null;
2708
                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
2709
                $signals[] = $params['JScode'];
2710
            } else {
2711
                switch ($set) {
2712
                    case 'updatePageTree':
2713
                        $signals[] = '
2714
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) {
2715
									top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
2716
								}
2717
							';
2718
                        break;
2719
                    case 'updateFolderTree':
2720
                        $signals[] = '
2721
								if (top && top.nav_frame && top.nav_frame.location) {
2722
									top.nav_frame.location.reload(true);
2723
								}';
2724
                        break;
2725
                    case 'updateModuleMenu':
2726
                        $signals[] = '
2727
								if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) {
2728
									top.TYPO3.ModuleMenu.App.refreshMenu();
2729
								}';
2730
                        break;
2731
                    case 'updateTopbar':
2732
                        $signals[] = '
2733
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) {
2734
									top.TYPO3.Backend.Topbar.refresh();
2735
								}';
2736
                        break;
2737
                }
2738
            }
2739
        }
2740
        $content = implode(LF, $signals);
2741
        // For backwards compatibility, should be replaced
2742
        self::setUpdateSignal();
2743
        return $content;
2744
    }
2745
2746
    /**
2747
     * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
2748
     * This is kind of session variable management framework for the backend users.
2749
     * 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
2750
     * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules.
2751
     *
2752
     * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus.
2753
     * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus.
2754
     * @param string $modName modName is the name of this module. Used to get the correct module data.
2755
     * @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.
2756
     * @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.
2757
     * @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)
2758
     * @throws \RuntimeException
2759
     * @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
2760
     */
2761
    public static function getModuleData(
2762
        $MOD_MENU,
2763
        $CHANGED_SETTINGS,
2764
        $modName,
2765
        $type = '',
2766
        $dontValidateList = '',
2767
        $setDefaultList = ''
2768
    ) {
2769
        if ($modName && is_string($modName)) {
2770
            // Getting stored user-data from this module:
2771
            $beUser = static::getBackendUserAuthentication();
2772
            $settings = $beUser->getModuleData($modName, $type);
2773
            $changed = 0;
2774
            if (!is_array($settings)) {
2775
                $changed = 1;
2776
                $settings = [];
2777
            }
2778
            if (is_array($MOD_MENU)) {
0 ignored issues
show
introduced by
The condition is_array($MOD_MENU) is always true.
Loading history...
2779
                foreach ($MOD_MENU as $key => $var) {
2780
                    // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array.
2781
                    if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) {
2782
                        if (is_array($CHANGED_SETTINGS[$key])) {
2783
                            $serializedSettings = serialize($CHANGED_SETTINGS[$key]);
2784
                            if ((string)$settings[$key] !== $serializedSettings) {
2785
                                $settings[$key] = $serializedSettings;
2786
                                $changed = 1;
2787
                            }
2788
                        } else {
2789
                            if ((string)$settings[$key] !== (string)$CHANGED_SETTINGS[$key]) {
2790
                                $settings[$key] = $CHANGED_SETTINGS[$key];
2791
                                $changed = 1;
2792
                            }
2793
                        }
2794
                    }
2795
                    // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted
2796
                    if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) {
2797
                        // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted.
2798
                        if (is_array($settings[$key]) || !isset($MOD_MENU[$key][$settings[$key]])) {
2799
                            $settings[$key] = (string)key($var);
2800
                            $changed = 1;
2801
                        }
2802
                    }
2803
                    // Sets default values (only strings/checkboxes, not menus)
2804
                    if ($setDefaultList && !is_array($var)) {
2805
                        if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) {
2806
                            $settings[$key] = (string)$var;
2807
                        }
2808
                    }
2809
                }
2810
            } else {
2811
                throw new \RuntimeException('No menu', 1568119229);
2812
            }
2813
            if ($changed) {
2814
                $beUser->pushModuleData($modName, $settings);
2815
            }
2816
            return $settings;
2817
        }
2818
        throw new \RuntimeException('Wrong module name "' . $modName . '"', 1568119221);
2819
    }
2820
2821
    /*******************************************
2822
     *
2823
     * Core
2824
     *
2825
     *******************************************/
2826
    /**
2827
     * Unlock or Lock a record from $table with $uid
2828
     * If $table and $uid is not set, then all locking for the current BE_USER is removed!
2829
     *
2830
     * @param string $table Table name
2831
     * @param int $uid Record uid
2832
     * @param int $pid Record pid
2833
     * @internal
2834
     */
2835
    public static function lockRecords($table = '', $uid = 0, $pid = 0)
2836
    {
2837
        $beUser = static::getBackendUserAuthentication();
2838
        if (isset($beUser->user['uid'])) {
2839
            $userId = (int)$beUser->user['uid'];
2840
            if ($table && $uid) {
2841
                $fieldsValues = [
2842
                    'userid' => $userId,
2843
                    'feuserid' => 0,
2844
                    'tstamp' => $GLOBALS['EXEC_TIME'],
2845
                    'record_table' => $table,
2846
                    'record_uid' => $uid,
2847
                    'username' => $beUser->user['username'],
2848
                    'record_pid' => $pid
2849
                ];
2850
                GeneralUtility::makeInstance(ConnectionPool::class)
2851
                    ->getConnectionForTable('sys_lockedrecords')
2852
                    ->insert(
2853
                        'sys_lockedrecords',
2854
                        $fieldsValues
2855
                    );
2856
            } else {
2857
                GeneralUtility::makeInstance(ConnectionPool::class)
2858
                    ->getConnectionForTable('sys_lockedrecords')
2859
                    ->delete(
2860
                        'sys_lockedrecords',
2861
                        ['userid' => (int)$userId]
2862
                    );
2863
            }
2864
        }
2865
    }
2866
2867
    /**
2868
     * Returns information about whether the record from table, $table, with uid, $uid is currently locked
2869
     * (edited by another user - which should issue a warning).
2870
     * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts
2871
     * are activated - which means that a user CAN have a record "open" without having it locked.
2872
     * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient.
2873
     *
2874
     * @param string $table Table name
2875
     * @param int $uid Record uid
2876
     * @return array|bool
2877
     * @internal
2878
     */
2879
    public static function isRecordLocked($table, $uid)
2880
    {
2881
        $runtimeCache = self::getRuntimeCache();
2882
        $cacheId = 'backend-recordLocked';
2883
        $recordLockedCache = $runtimeCache->get($cacheId);
2884
        if ($recordLockedCache !== false) {
2885
            $lockedRecords = $recordLockedCache;
2886
        } else {
2887
            $lockedRecords = [];
2888
2889
            $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords');
2890
            $result = $queryBuilder
2891
                ->select('*')
2892
                ->from('sys_lockedrecords')
2893
                ->where(
2894
                    $queryBuilder->expr()->neq(
2895
                        'sys_lockedrecords.userid',
2896
                        $queryBuilder->createNamedParameter(
2897
                            static::getBackendUserAuthentication()->user['uid'],
2898
                            \PDO::PARAM_INT
2899
                        )
2900
                    ),
2901
                    $queryBuilder->expr()->gt(
2902
                        'sys_lockedrecords.tstamp',
2903
                        $queryBuilder->createNamedParameter(
2904
                            $GLOBALS['EXEC_TIME'] - 2 * 3600,
2905
                            \PDO::PARAM_INT
2906
                        )
2907
                    )
2908
                )
2909
                ->execute();
2910
2911
            $lang = static::getLanguageService();
2912
            while ($row = $result->fetch()) {
2913
                // Get the type of the user that locked this record:
2914
                if ($row['userid']) {
2915
                    $userTypeLabel = 'beUser';
2916
                } elseif ($row['feuserid']) {
2917
                    $userTypeLabel = 'feUser';
2918
                } else {
2919
                    $userTypeLabel = 'user';
2920
                }
2921
                $userType = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel);
2922
                // Get the username (if available):
2923
                if ($row['username']) {
2924
                    $userName = $row['username'];
2925
                } else {
2926
                    $userName = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.unknownUser');
2927
                }
2928
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row;
2929
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf(
2930
                    $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'),
2931
                    $userType,
2932
                    $userName,
2933
                    self::calcAge(
2934
                        $GLOBALS['EXEC_TIME'] - $row['tstamp'],
2935
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2936
                    )
2937
                );
2938
                if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) {
2939
                    $lockedRecords['pages:' . $row['record_pid']]['msg'] = sprintf(
2940
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'),
2941
                        $userType,
2942
                        $userName,
2943
                        self::calcAge(
2944
                            $GLOBALS['EXEC_TIME'] - $row['tstamp'],
2945
                            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2946
                        )
2947
                    );
2948
                }
2949
            }
2950
            $runtimeCache->set($cacheId, $lockedRecords);
2951
        }
2952
2953
        return $lockedRecords[$table . ':' . $uid] ?? false;
2954
    }
2955
2956
    /**
2957
     * Returns TSConfig for the TCEFORM object in Page TSconfig.
2958
     * Used in TCEFORMs
2959
     *
2960
     * @param string $table Table name present in TCA
2961
     * @param array $row Row from table
2962
     * @return array
2963
     */
2964
    public static function getTCEFORM_TSconfig($table, $row)
2965
    {
2966
        $res = [];
2967
        // Get main config for the table
2968
        [$TScID, $cPid] = self::getTSCpid($table, $row['uid'], $row['pid']);
2969
        if ($TScID >= 0) {
2970
            $tsConfig = static::getPagesTSconfig($TScID)['TCEFORM.'][$table . '.'] ?? [];
2971
            $typeVal = self::getTCAtypeValue($table, $row);
2972
            foreach ($tsConfig as $key => $val) {
2973
                if (is_array($val)) {
2974
                    $fieldN = substr($key, 0, -1);
2975
                    $res[$fieldN] = $val;
2976
                    unset($res[$fieldN]['types.']);
2977
                    if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'])) {
2978
                        ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']);
2979
                    }
2980
                }
2981
            }
2982
        }
2983
        $res['_CURRENT_PID'] = $cPid;
2984
        $res['_THIS_UID'] = $row['uid'];
2985
        // So the row will be passed to foreign_table_where_query()
2986
        $res['_THIS_ROW'] = $row;
2987
        return $res;
2988
    }
2989
2990
    /**
2991
     * Find the real PID of the record (with $uid from $table).
2992
     * 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).
2993
     *
2994
     * @param string $table Table name
2995
     * @param int $uid Record uid
2996
     * @param int $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return
2997
     * @return int
2998
     * @internal
2999
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord()
3000
     * @see \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid()
3001
     */
3002
    public static function getTSconfig_pidValue($table, $uid, $pid)
3003
    {
3004
        // If pid is an integer this takes precedence in our lookup.
3005
        if (MathUtility::canBeInterpretedAsInteger($pid)) {
3006
            $thePidValue = (int)$pid;
3007
            // If ref to another record, look that record up.
3008
            if ($thePidValue < 0) {
3009
                $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

3009
                $pidRec = self::getRecord($table, /** @scrutinizer ignore-type */ abs($thePidValue), 'pid');
Loading history...
3010
                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
3011
            }
3012
        } else {
3013
            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
3014
            $rr = self::getRecord($table, $uid);
3015
            $thePidValue = null;
3016
            if (is_array($rr)) {
3017
                // First check if the pid is -1 which means it is a workspaced element. Get the "real" record:
3018
                if ($rr['pid'] == '-1') {
3019
                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
3020
                    if (is_array($rr)) {
3021
                        $thePidValue = $rr['pid'];
3022
                    }
3023
                } else {
3024
                    // Returning the "pid" of the record
3025
                    $thePidValue = $rr['pid'];
3026
                }
3027
            }
3028
            if (!$thePidValue) {
3029
                // Returns -1 if the record with this pid was not found.
3030
                $thePidValue = -1;
3031
            }
3032
        }
3033
        return $thePidValue;
3034
    }
3035
3036
    /**
3037
     * Return the real pid of a record and caches the result.
3038
     * The non-cached method needs database queries to do the job, so this method
3039
     * can be used if code sometimes calls the same record multiple times to save
3040
     * some queries. This should not be done if the calling code may change the
3041
     * same record meanwhile.
3042
     *
3043
     * @param string $table Tablename
3044
     * @param string $uid UID value
3045
     * @param string $pid PID value
3046
     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
3047
     */
3048
    public static function getTSCpidCached($table, $uid, $pid)
3049
    {
3050
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3051
        $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: [];
3052
        $key = $table . ':' . $uid . ':' . $pid;
3053
        if (!isset($firstLevelCache[$key])) {
3054
            $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

3054
            $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

3054
            $firstLevelCache[$key] = static::getTSCpid($table, /** @scrutinizer ignore-type */ $uid, $pid);
Loading history...
3055
            $runtimeCache->set('backendUtilityTscPidCached', $firstLevelCache);
3056
        }
3057
        return $firstLevelCache[$key];
3058
    }
3059
3060
    /**
3061
     * 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.
3062
     *
3063
     * @param string $table Table name
3064
     * @param int $uid Record uid
3065
     * @param int $pid Record pid
3066
     * @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,
3067
     * second value is the PID value for TSconfig (uid if table is pages, otherwise the pid)
3068
     * @internal
3069
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::setHistory()
3070
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::process_datamap()
3071
     */
3072
    public static function getTSCpid($table, $uid, $pid)
3073
    {
3074
        // If pid is negative (referring to another record) the pid of the other record is fetched and returned.
3075
        $cPid = self::getTSconfig_pidValue($table, $uid, $pid);
3076
        // $TScID is the id of $table = pages, else it's the pid of the record.
3077
        $TScID = $table === 'pages' && MathUtility::canBeInterpretedAsInteger($uid) ? $uid : $cPid;
3078
        return [$TScID, $cPid];
3079
    }
3080
3081
    /**
3082
     * Returns soft-reference parser for the softRef processing type
3083
     * Usage: $softRefObj = BackendUtility::softRefParserObj('[parser key]');
3084
     *
3085
     * @param string $spKey softRef parser key
3086
     * @return mixed If available, returns Soft link parser object, otherwise false.
3087
     * @internal should only be used from within TYPO3 Core
3088
     */
3089
    public static function softRefParserObj($spKey)
3090
    {
3091
        $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser'][$spKey] ?? false;
3092
        if ($className) {
3093
            return GeneralUtility::makeInstance($className);
3094
        }
3095
        return false;
3096
    }
3097
3098
    /**
3099
     * Gets an instance of the runtime cache.
3100
     *
3101
     * @return FrontendInterface
3102
     */
3103
    protected static function getRuntimeCache()
3104
    {
3105
        return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3106
    }
3107
3108
    /**
3109
     * Returns array of soft parser references
3110
     *
3111
     * @param string $parserList softRef parser list
3112
     * @return array|bool Array where the parser key is the key and the value is the parameter string, FALSE if no parsers were found
3113
     * @throws \InvalidArgumentException
3114
     * @internal should only be used from within TYPO3 Core
3115
     */
3116
    public static function explodeSoftRefParserList($parserList)
3117
    {
3118
        // Return immediately if list is blank:
3119
        if ((string)$parserList === '') {
3120
            return false;
3121
        }
3122
3123
        $runtimeCache = self::getRuntimeCache();
3124
        $cacheId = 'backend-softRefList-' . md5($parserList);
3125
        $parserListCache = $runtimeCache->get($cacheId);
3126
        if ($parserListCache !== false) {
3127
            return $parserListCache;
3128
        }
3129
3130
        // Otherwise parse the list:
3131
        $keyList = GeneralUtility::trimExplode(',', $parserList, true);
3132
        $output = [];
3133
        foreach ($keyList as $val) {
3134
            $reg = [];
3135
            if (preg_match('/^([[:alnum:]_-]+)\\[(.*)\\]$/', $val, $reg)) {
3136
                $output[$reg[1]] = GeneralUtility::trimExplode(';', $reg[2], true);
3137
            } else {
3138
                $output[$val] = '';
3139
            }
3140
        }
3141
        $runtimeCache->set($cacheId, $output);
3142
        return $output;
3143
    }
3144
3145
    /**
3146
     * Returns TRUE if $modName is set and is found as a main- or submodule in $TBE_MODULES array
3147
     *
3148
     * @param string $modName Module name
3149
     * @return bool
3150
     */
3151
    public static function isModuleSetInTBE_MODULES($modName)
3152
    {
3153
        $loaded = [];
3154
        foreach ($GLOBALS['TBE_MODULES'] as $mkey => $list) {
3155
            $loaded[$mkey] = 1;
3156
            if (!is_array($list) && trim($list)) {
3157
                $subList = GeneralUtility::trimExplode(',', $list, true);
3158
                foreach ($subList as $skey) {
3159
                    $loaded[$mkey . '_' . $skey] = 1;
3160
                }
3161
            }
3162
        }
3163
        return $modName && isset($loaded[$modName]);
3164
    }
3165
3166
    /**
3167
     * Counting references to a record/file
3168
     *
3169
     * @param string $table Table name (or "_FILE" if its a file)
3170
     * @param string $ref Reference: If table, then int-uid, if _FILE, then file reference (relative to Environment::getPublicPath())
3171
     * @param string $msg Message with %s, eg. "There were %s records pointing to this file!
3172
     * @param string|null $count Reference count
3173
     * @return string Output string (or int count value if no msg string specified)
3174
     */
3175
    public static function referenceCount($table, $ref, $msg = '', $count = null)
3176
    {
3177
        if ($count === null) {
3178
3179
            // Build base query
3180
            $queryBuilder = static::getQueryBuilderForTable('sys_refindex');
3181
            $queryBuilder
3182
                ->count('*')
3183
                ->from('sys_refindex')
3184
                ->where(
3185
                    $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
3186
                    $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
3187
                );
3188
3189
            // Look up the path:
3190
            if ($table === '_FILE') {
3191
                if (!GeneralUtility::isFirstPartOfStr($ref, Environment::getPublicPath())) {
3192
                    return '';
3193
                }
3194
3195
                $ref = PathUtility::stripPathSitePrefix($ref);
3196
                $queryBuilder->andWhere(
3197
                    $queryBuilder->expr()->eq('ref_string', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_STR))
3198
                );
3199
            } else {
3200
                $queryBuilder->andWhere(
3201
                    $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT))
3202
                );
3203
                if ($table === 'sys_file') {
3204
                    $queryBuilder->andWhere($queryBuilder->expr()->neq('tablename', $queryBuilder->quote('sys_file_metadata')));
3205
                }
3206
            }
3207
3208
            $count = $queryBuilder->execute()->fetchColumn(0);
3209
        }
3210
3211
        if ($count) {
3212
            return $msg ? sprintf($msg, $count) : $count;
3213
        }
3214
        return $msg ? '' : 0;
3215
    }
3216
3217
    /**
3218
     * Counting translations of records
3219
     *
3220
     * @param string $table Table name
3221
     * @param string $ref Reference: the record's uid
3222
     * @param string $msg Message with %s, eg. "This record has %s translation(s) which will be deleted, too!
3223
     * @return string Output string (or int count value if no msg string specified)
3224
     */
3225
    public static function translationCount($table, $ref, $msg = '')
3226
    {
3227
        $count = null;
3228
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']
3229
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
3230
        ) {
3231
            $queryBuilder = static::getQueryBuilderForTable($table);
3232
            $queryBuilder->getRestrictions()
3233
                ->removeAll()
3234
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3235
3236
            $count = (int)$queryBuilder
3237
                ->count('*')
3238
                ->from($table)
3239
                ->where(
3240
                    $queryBuilder->expr()->eq(
3241
                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3242
                        $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)
3243
                    ),
3244
                    $queryBuilder->expr()->neq(
3245
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
3246
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3247
                    )
3248
                )
3249
                ->execute()
3250
                ->fetchColumn(0);
3251
        }
3252
3253
        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...
3254
            return sprintf($msg, $count);
3255
        }
3256
3257
        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...
3258
            return $msg ? sprintf($msg, $count) : $count;
3259
        }
3260
        return $msg ? '' : 0;
3261
    }
3262
3263
    /*******************************************
3264
     *
3265
     * Workspaces / Versioning
3266
     *
3267
     *******************************************/
3268
    /**
3269
     * Select all versions of a record, ordered by latest created version (uid DESC)
3270
     *
3271
     * @param string $table Table name to select from
3272
     * @param int $uid Record uid for which to find versions.
3273
     * @param string $fields Field list to select
3274
     * @param int|null $workspace Search in workspace ID and Live WS, if 0 search only in LiveWS, if NULL search in all WS.
3275
     * @param bool $includeDeletedRecords If set, deleted-flagged versions are included! (Only for clean-up script!)
3276
     * @param array $row The current record
3277
     * @return array|null Array of versions of table/uid
3278
     * @internal should only be used from within TYPO3 Core
3279
     */
3280
    public static function selectVersionsOfRecord(
3281
        $table,
3282
        $uid,
3283
        $fields = '*',
3284
        $workspace = 0,
3285
        $includeDeletedRecords = false,
3286
        $row = null
3287
    ) {
3288
        $outputRows = [];
3289
        if (static::isTableWorkspaceEnabled($table)) {
3290
            if (is_array($row) && !$includeDeletedRecords) {
3291
                $row['_CURRENT_VERSION'] = true;
3292
                $outputRows[] = $row;
3293
            } else {
3294
                // Select UID version:
3295
                $row = self::getRecord($table, $uid, $fields, '', !$includeDeletedRecords);
3296
                // Add rows to output array:
3297
                if ($row) {
3298
                    $row['_CURRENT_VERSION'] = true;
3299
                    $outputRows[] = $row;
3300
                }
3301
            }
3302
3303
            $queryBuilder = static::getQueryBuilderForTable($table);
3304
            $queryBuilder->getRestrictions()->removeAll();
3305
3306
            // build fields to select
3307
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3308
3309
            $queryBuilder
3310
                ->from($table)
3311
                ->where(
3312
                    $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
3313
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
3314
                )
3315
                ->orderBy('uid', 'DESC');
3316
3317
            if (!$includeDeletedRecords) {
3318
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3319
            }
3320
3321
            if ($workspace === 0) {
3322
                // Only in Live WS
3323
                $queryBuilder->andWhere(
3324
                    $queryBuilder->expr()->eq(
3325
                        't3ver_wsid',
3326
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3327
                    )
3328
                );
3329
            } elseif ($workspace !== null) {
3330
                // In Live WS and Workspace with given ID
3331
                $queryBuilder->andWhere(
3332
                    $queryBuilder->expr()->in(
3333
                        't3ver_wsid',
3334
                        $queryBuilder->createNamedParameter([0, (int)$workspace], Connection::PARAM_INT_ARRAY)
3335
                    )
3336
                );
3337
            }
3338
3339
            $rows = $queryBuilder->execute()->fetchAll();
3340
3341
            // Add rows to output array:
3342
            if (is_array($rows)) {
0 ignored issues
show
introduced by
The condition is_array($rows) is always true.
Loading history...
3343
                $outputRows = array_merge($outputRows, $rows);
3344
            }
3345
            return $outputRows;
3346
        }
3347
        return null;
3348
    }
3349
3350
    /**
3351
     * Find page-tree PID for versionized record
3352
     * Used whenever you are tracking something back, like making the root line.
3353
     * Will only translate if the workspace of the input record matches that of the current user (unless flag set)
3354
     * Principle; Record offline! => Find online?
3355
     *
3356
     * If the record had its pid corrected to the online versions pid,
3357
     * then "_ORIG_pid" is set for moved records to the PID of the moved location.
3358
     *
3359
     * @param string $table Table name
3360
     * @param array $rr Record array passed by reference. As minimum, "pid" and "uid" fields must exist! "t3ver_oid", "t3ver_state" and "t3ver_wsid" is nice and will save you a DB query.
3361
     * @param bool $ignoreWorkspaceMatch Ignore workspace match
3362
     * @see PageRepository::fixVersioningPid()
3363
     * @internal should only be used from within TYPO3 Core
3364
     * @deprecated will be removed in TYPO3 v12, use workspaceOL() or getRecordWSOL() directly to achieve the same result.
3365
     */
3366
    public static function fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch = false)
3367
    {
3368
        trigger_error('BackendUtility::fixVersioningPid() will be removed in TYPO3 v12, use BackendUtility::workspaceOL() or BackendUtility::getRecordWSOL() directly.', E_USER_DEPRECATED);
3369
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3370
            return;
3371
        }
3372
        if (!static::isTableWorkspaceEnabled($table)) {
3373
            return;
3374
        }
3375
        // Check that the input record is an offline version from a table that supports versioning
3376
        if (!is_array($rr)) {
0 ignored issues
show
introduced by
The condition is_array($rr) is always true.
Loading history...
3377
            return;
3378
        }
3379
        $incomingPid = $rr['pid'] ?? null;
3380
        // Check values for t3ver_oid and t3ver_wsid:
3381
        if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid']) && isset($rr['t3ver_state'])) {
3382
            // If "t3ver_oid" is already a field, just set this:
3383
            $oid = $rr['t3ver_oid'];
3384
            $workspaceId = (int)$rr['t3ver_wsid'];
3385
            $versionState = (int)$rr['t3ver_state'];
3386
        } else {
3387
            $oid = 0;
3388
            $workspaceId = 0;
3389
            $versionState = 0;
3390
            // Otherwise we have to expect "uid" to be in the record and look up based on this:
3391
            $newPidRec = self::getRecord($table, $rr['uid'], 'pid,t3ver_oid,t3ver_wsid,t3ver_state');
3392
            if (is_array($newPidRec)) {
3393
                $incomingPid = $newPidRec['pid'];
3394
                $oid = $newPidRec['t3ver_oid'];
3395
                $workspaceId = $newPidRec['t3ver_wsid'];
3396
                $versionState = $newPidRec['t3ver_state'];
3397
            }
3398
        }
3399
        if ($oid && ($ignoreWorkspaceMatch || (static::getBackendUserAuthentication() instanceof BackendUserAuthentication && $workspaceId === (int)static::getBackendUserAuthentication()->workspace))) {
3400
            // Use moved PID in case of move pointer
3401
            if ($versionState === VersionState::MOVE_POINTER) {
3402
                if ($incomingPid !== null) {
3403
                    $movedPageIdInWorkspace = $incomingPid;
3404
                } else {
3405
                    $versionedMovePointer = self::getRecord($table, $rr['uid'], 'pid');
3406
                    $movedPageIdInWorkspace = $versionedMovePointer['pid'];
3407
                }
3408
                $rr['_ORIG_pid'] = $incomingPid;
3409
                $rr['pid'] = $movedPageIdInWorkspace;
3410
            }
3411
        }
3412
    }
3413
3414
    /**
3415
     * Workspace Preview Overlay.
3416
     *
3417
     * Generally ALWAYS used when records are selected based on uid or pid.
3418
     * Principle; Record online! => Find offline?
3419
     * The function MAY set $row to FALSE. This happens if a moved record is given and
3420
     * $unsetMovePointers is set to true. In other words, you should check if the input record
3421
     * is still an array afterwards when using this function.
3422
     *
3423
     * If the versioned record is a moved record the "pid" value will then contain the newly moved location
3424
     * and "ORIG_pid" will contain the live pid.
3425
     *
3426
     * @param string $table Table name
3427
     * @param array $row Record by reference. At least "uid", "pid", "t3ver_oid" and "t3ver_state" must be set. Keys not prefixed with '_' are used as field names in SQL.
3428
     * @param int $wsid Workspace ID, if not specified will use static::getBackendUserAuthentication()->workspace
3429
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
3430
     * @see fixVersioningPid()
3431
     */
3432
    public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false)
3433
    {
3434
        if (!ExtensionManagementUtility::isLoaded('workspaces') || !is_array($row) || !static::isTableWorkspaceEnabled($table)) {
3435
            return;
3436
        }
3437
3438
        // Initialize workspace ID
3439
        $wsid = (int)$wsid;
3440
        if ($wsid === -99 && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3441
            $wsid = (int)static::getBackendUserAuthentication()->workspace;
3442
        }
3443
        if ($wsid === 0) {
3444
            // Return early if in live workspace
3445
            return;
3446
        }
3447
3448
        // Check if input record is a moved record
3449
        $incomingRecordIsAMoveVersion = false;
3450
        if (isset($row['t3ver_oid'], $row['t3ver_state'])
3451
            && $row['t3ver_oid'] > 0
3452
            && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER
3453
        ) {
3454
            // @todo: This handling needs a review, together with the 4th param $unsetMovePointers
3455
            $incomingRecordIsAMoveVersion = true;
3456
        }
3457
3458
        $wsAlt = self::getWorkspaceVersionOfRecord(
3459
            $wsid,
3460
            $table,
3461
            $row['uid'],
3462
            implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3463
        );
3464
3465
        // If version was found, swap the default record with that one.
3466
        if (is_array($wsAlt)) {
3467
            // If t3ver_state is not found, then find it... (but we like best if it is here...)
3468
            if (!isset($wsAlt['t3ver_state'])) {
3469
                $stateRec = self::getRecord($table, $wsAlt['uid'], 't3ver_state');
3470
                $versionState = VersionState::cast($stateRec['t3ver_state']);
3471
            } else {
3472
                $versionState = VersionState::cast($wsAlt['t3ver_state']);
3473
            }
3474
            // Check if this is in move-state
3475
            if ($versionState->equals(VersionState::MOVE_POINTER)) {
3476
                // @todo Same problem as frontend in versionOL(). See TODO point there and todo above.
3477
                if (!$incomingRecordIsAMoveVersion && $unsetMovePointers) {
3478
                    $row = false;
3479
                    return;
3480
                }
3481
                // When a moved record is found the "PID" value contains the newly moved location
3482
                // Whereas the _ORIG_pid field contains the PID of the live version
3483
                $wsAlt['_ORIG_pid'] = $row['pid'];
3484
            }
3485
            // Swap UID
3486
            $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
3487
            $wsAlt['uid'] = $row['uid'];
3488
            // Backend css class:
3489
            $wsAlt['_CSSCLASS'] = 'ver-element';
3490
            // Changing input record to the workspace version alternative:
3491
            $row = $wsAlt;
3492
        }
3493
    }
3494
3495
    /**
3496
     * Select the workspace version of a record, if exists
3497
     *
3498
     * @param int $workspace Workspace ID
3499
     * @param string $table Table name to select from
3500
     * @param int $uid Record uid for which to find workspace version.
3501
     * @param string $fields Field list to select
3502
     * @return array|bool If found, return record, otherwise false
3503
     */
3504
    public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*')
3505
    {
3506
        if (ExtensionManagementUtility::isLoaded('workspaces')) {
3507
            if ($workspace !== 0 && self::isTableWorkspaceEnabled($table)) {
3508
3509
                // Select workspace version of record:
3510
                $queryBuilder = static::getQueryBuilderForTable($table);
3511
                $queryBuilder->getRestrictions()
3512
                    ->removeAll()
3513
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3514
3515
                // build fields to select
3516
                $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3517
3518
                $row = $queryBuilder
3519
                    ->from($table)
3520
                    ->where(
3521
                        $queryBuilder->expr()->eq(
3522
                            't3ver_oid',
3523
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3524
                        ),
3525
                        $queryBuilder->expr()->eq(
3526
                            't3ver_wsid',
3527
                            $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
3528
                        )
3529
                    )
3530
                    ->execute()
3531
                    ->fetch();
3532
3533
                return $row;
3534
            }
3535
        }
3536
        return false;
3537
    }
3538
3539
    /**
3540
     * Returns live version of record
3541
     *
3542
     * @param string $table Table name
3543
     * @param int $uid Record UID of draft, offline version
3544
     * @param string $fields Field list, default is *
3545
     * @return array|null If found, the record, otherwise NULL
3546
     */
3547
    public static function getLiveVersionOfRecord($table, $uid, $fields = '*')
3548
    {
3549
        $liveVersionId = self::getLiveVersionIdOfRecord($table, $uid);
3550
        if ($liveVersionId !== null) {
3551
            return self::getRecord($table, $liveVersionId, $fields);
3552
        }
3553
        return null;
3554
    }
3555
3556
    /**
3557
     * Gets the id of the live version of a record.
3558
     *
3559
     * @param string $table Name of the table
3560
     * @param int $uid Uid of the offline/draft record
3561
     * @return int|null The id of the live version of the record (or NULL if nothing was found)
3562
     * @internal should only be used from within TYPO3 Core
3563
     */
3564
    public static function getLiveVersionIdOfRecord($table, $uid)
3565
    {
3566
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3567
            return null;
3568
        }
3569
        $liveVersionId = null;
3570
        if (self::isTableWorkspaceEnabled($table)) {
3571
            $currentRecord = self::getRecord($table, $uid, 'pid,t3ver_oid');
3572
            if (is_array($currentRecord) && (int)$currentRecord['t3ver_oid'] > 0) {
3573
                $liveVersionId = $currentRecord['t3ver_oid'];
3574
            }
3575
        }
3576
        return $liveVersionId;
3577
    }
3578
3579
    /**
3580
     * Performs mapping of new uids to new versions UID in case of import inside a workspace.
3581
     *
3582
     * @param string $table Table name
3583
     * @param int $uid Record uid (of live record placeholder)
3584
     * @return int Uid of offline version if any, otherwise live uid.
3585
     * @internal should only be used from within TYPO3 Core
3586
     */
3587
    public static function wsMapId($table, $uid)
3588
    {
3589
        $wsRec = null;
3590
        if (static::getBackendUserAuthentication() instanceof BackendUserAuthentication) {
3591
            $wsRec = self::getWorkspaceVersionOfRecord(
3592
                static::getBackendUserAuthentication()->workspace,
3593
                $table,
3594
                $uid,
3595
                'uid'
3596
            );
3597
        }
3598
        return is_array($wsRec) ? $wsRec['uid'] : $uid;
3599
    }
3600
3601
    /*******************************************
3602
     *
3603
     * Miscellaneous
3604
     *
3605
     *******************************************/
3606
3607
    /**
3608
     * Creates ADMCMD parameters for the "viewpage" extension / frontend
3609
     *
3610
     * @param array $pageInfo Page record
3611
     * @param \TYPO3\CMS\Core\Context\Context $context
3612
     * @return string Query-parameters
3613
     * @internal
3614
     */
3615
    public static function ADMCMD_previewCmds($pageInfo, Context $context)
3616
    {
3617
        $simUser = '';
3618
        $simTime = '';
3619
        if ($pageInfo['fe_group'] > 0) {
3620
            $simUser = '&ADMCMD_simUser=' . $pageInfo['fe_group'];
3621
        } elseif ((int)$pageInfo['fe_group'] === -2) {
3622
            // -2 means "show at any login". We simulate first available fe_group.
3623
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3624
                ->getQueryBuilderForTable('fe_groups');
3625
            $queryBuilder->getRestrictions()
3626
                ->removeAll()
3627
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3628
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
3629
3630
            $activeFeGroupRow = $queryBuilder->select('uid')
3631
                ->from('fe_groups')
3632
                ->execute()
3633
                ->fetch();
3634
3635
            if (!empty($activeFeGroupRow)) {
3636
                $simUser = '&ADMCMD_simUser=' . $activeFeGroupRow['uid'];
3637
            }
3638
        }
3639
        $startTime = (int)$pageInfo['starttime'];
3640
        $endTime = (int)$pageInfo['endtime'];
3641
        if ($startTime > $GLOBALS['EXEC_TIME']) {
3642
            // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
3643
            // an URL for it
3644
            $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $startTime));
3645
            $context->setAspect('date', $dateAspect);
3646
            $simTime = '&ADMCMD_simTime=' . $startTime;
3647
        }
3648
        if ($endTime < $GLOBALS['EXEC_TIME'] && $endTime !== 0) {
3649
            // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
3650
            // in turn PageRouter will generate an URL for it
3651
            $dateAspect = GeneralUtility::makeInstance(
3652
                DateTimeAspect::class,
3653
                new \DateTimeImmutable('@' . ($endTime - 1))
3654
            );
3655
            $context->setAspect('date', $dateAspect);
3656
            $simTime = '&ADMCMD_simTime=' . ($endTime - 1);
3657
        }
3658
        return $simUser . $simTime;
3659
    }
3660
3661
    /**
3662
     * Returns the name of the backend script relative to the TYPO3 main directory.
3663
     *
3664
     * @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.
3665
     * @return string The name of the backend script relative to the TYPO3 main directory.
3666
     * @internal should only be used from within TYPO3 Core
3667
     */
3668
    public static function getBackendScript($interface = '')
3669
    {
3670
        if (!$interface) {
3671
            $interface = static::getBackendUserAuthentication()->uc['interfaceSetup'];
3672
        }
3673
        switch ($interface) {
3674
            case 'frontend':
3675
                $script = '../.';
3676
                break;
3677
            case 'backend':
3678
            default:
3679
                $script = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('main');
3680
        }
3681
        return $script;
3682
    }
3683
3684
    /**
3685
     * Determines whether a table is enabled for workspaces.
3686
     *
3687
     * @param string $table Name of the table to be checked
3688
     * @return bool
3689
     */
3690
    public static function isTableWorkspaceEnabled($table)
3691
    {
3692
        return !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']);
3693
    }
3694
3695
    /**
3696
     * Gets the TCA configuration of a field.
3697
     *
3698
     * @param string $table Name of the table
3699
     * @param string $field Name of the field
3700
     * @return array
3701
     */
3702
    public static function getTcaFieldConfiguration($table, $field)
3703
    {
3704
        $configuration = [];
3705
        if (isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
3706
            $configuration = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3707
        }
3708
        return $configuration;
3709
    }
3710
3711
    /**
3712
     * Whether to ignore restrictions on a web-mount of a table.
3713
     * The regular behaviour is that records to be accessed need to be
3714
     * in a valid user's web-mount.
3715
     *
3716
     * @param string $table Name of the table
3717
     * @return bool
3718
     */
3719
    public static function isWebMountRestrictionIgnored($table)
3720
    {
3721
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreWebMountRestriction']);
3722
    }
3723
3724
    /**
3725
     * Whether to ignore restrictions on root-level records.
3726
     * The regular behaviour is that records on the root-level (page-id 0)
3727
     * only can be accessed by admin users.
3728
     *
3729
     * @param string $table Name of the table
3730
     * @return bool
3731
     */
3732
    public static function isRootLevelRestrictionIgnored($table)
3733
    {
3734
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']);
3735
    }
3736
3737
    /**
3738
     * @param string $table
3739
     * @return Connection
3740
     */
3741
    protected static function getConnectionForTable($table)
3742
    {
3743
        return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
3744
    }
3745
3746
    /**
3747
     * @param string $table
3748
     * @return QueryBuilder
3749
     */
3750
    protected static function getQueryBuilderForTable($table)
3751
    {
3752
        return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3753
    }
3754
3755
    /**
3756
     * @return LoggerInterface
3757
     */
3758
    protected static function getLogger()
3759
    {
3760
        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
3761
    }
3762
3763
    /**
3764
     * @return LanguageService
3765
     */
3766
    protected static function getLanguageService()
3767
    {
3768
        return $GLOBALS['LANG'];
3769
    }
3770
3771
    /**
3772
     * @return BackendUserAuthentication|null
3773
     */
3774
    protected static function getBackendUserAuthentication()
3775
    {
3776
        return $GLOBALS['BE_USER'] ?? null;
3777
    }
3778
}
3779