Passed
Push — master ( f99186...373b25 )
by
unknown
15:01
created

BackendUtility::getWorkspaceVersionOfRecord()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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

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

1460
                if (trim(/** @scrutinizer ignore-type */ $recordTitle) === '') {
Loading history...
1461
                    $recordTitle = self::getNoRecordTitle($prep);
1462
                }
1463
            }
1464
        }
1465
1466
        return $recordTitle;
1467
    }
1468
1469
    /**
1470
     * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>,
1471
     * which offers a tooltip with the original title when moving mouse over it.
1472
     *
1473
     * @param string $title The title string to be cropped
1474
     * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
1475
     * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
1476
     */
1477
    public static function getRecordTitlePrep($title, $titleLength = 0)
1478
    {
1479
        // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
1480
        if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
1481
            $titleLength = static::getBackendUserAuthentication()->uc['titleLen'];
1482
        }
1483
        $titleOrig = htmlspecialchars($title);
1484
        $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
1485
        // If title was cropped, offer a tooltip:
1486
        if ($titleOrig != $title) {
1487
            $title = '<span title="' . $titleOrig . '">' . $title . '</span>';
1488
        }
1489
        return $title;
1490
    }
1491
1492
    /**
1493
     * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE.
1494
     *
1495
     * @param bool $prep Wrap result in <em>|</em>
1496
     * @return string Localized [No title] string
1497
     */
1498
    public static function getNoRecordTitle($prep = false)
1499
    {
1500
        $noTitle = '[' .
1501
            htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title'))
1502
            . ']';
1503
        if ($prep) {
1504
            $noTitle = '<em>' . $noTitle . '</em>';
1505
        }
1506
        return $noTitle;
1507
    }
1508
1509
    /**
1510
     * Returns a human readable output of a value from a record
1511
     * 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.
1512
     * $table/$col is tablename and fieldname
1513
     * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant)
1514
     *
1515
     * @param string $table Table name, present in TCA
1516
     * @param string $col Field name, present in TCA
1517
     * @param string $value The value of that field from a selected record
1518
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1519
     * @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")
1520
     * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown.
1521
     * @param int $uid Uid of the current record
1522
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1523
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1524
     * @throws \InvalidArgumentException
1525
     * @return string|null
1526
     */
1527
    public static function getProcessedValue(
1528
        $table,
1529
        $col,
1530
        $value,
1531
        $fixed_lgd_chars = 0,
1532
        $defaultPassthrough = false,
1533
        $noRecordLookup = false,
1534
        $uid = 0,
1535
        $forceResult = true,
1536
        $pid = 0
1537
    ) {
1538
        if ($col === 'uid') {
1539
            // uid is not in TCA-array
1540
            return $value;
1541
        }
1542
        // Check if table and field is configured
1543
        if (!isset($GLOBALS['TCA'][$table]['columns'][$col]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1544
            return null;
1545
        }
1546
        // Depending on the fields configuration, make a meaningful output value.
1547
        $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? [];
1548
        /*****************
1549
         *HOOK: pre-processing the human readable output from a record
1550
         ****************/
1551
        $referenceObject = new \stdClass();
1552
        $referenceObject->table = $table;
1553
        $referenceObject->fieldName = $col;
1554
        $referenceObject->uid = $uid;
1555
        $referenceObject->value = &$value;
1556
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) {
1557
            GeneralUtility::callUserFunction($_funcRef, $theColConf, $referenceObject);
1558
        }
1559
1560
        $l = '';
1561
        $lang = static::getLanguageService();
1562
        switch ((string)($theColConf['type'] ?? '')) {
1563
            case 'radio':
1564
                $l = self::getLabelFromItemlist($table, $col, $value);
1565
                $l = $lang->sL($l);
1566
                break;
1567
            case 'inline':
1568
            case 'select':
1569
                if (!empty($theColConf['MM'])) {
1570
                    if ($uid) {
1571
                        // Display the title of MM related records in lists
1572
                        if ($noRecordLookup) {
1573
                            $MMfields = [];
1574
                            $MMfields[] = $theColConf['foreign_table'] . '.uid';
1575
                        } else {
1576
                            $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1577
                            if (isset($GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'])) {
1578
                                foreach (GeneralUtility::trimExplode(
1579
                                    ',',
1580
                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'],
1581
                                    true
1582
                                ) as $f) {
1583
                                    $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1584
                                }
1585
                            }
1586
                        }
1587
                        /** @var RelationHandler $dbGroup */
1588
                        $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1589
                        $dbGroup->start(
1590
                            $value,
1591
                            $theColConf['foreign_table'],
1592
                            $theColConf['MM'],
1593
                            $uid,
1594
                            $table,
1595
                            $theColConf
1596
                        );
1597
                        $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1598
                        if (is_array($selectUids) && !empty($selectUids)) {
1599
                            $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1600
                            $queryBuilder->getRestrictions()
1601
                                ->removeAll()
1602
                                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1603
1604
                            $result = $queryBuilder
1605
                                ->select('uid', ...$MMfields)
1606
                                ->from($theColConf['foreign_table'])
1607
                                ->where(
1608
                                    $queryBuilder->expr()->in(
1609
                                        'uid',
1610
                                        $queryBuilder->createNamedParameter($selectUids, Connection::PARAM_INT_ARRAY)
1611
                                    )
1612
                                )
1613
                                ->execute();
1614
1615
                            $mmlA = [];
1616
                            while ($MMrow = $result->fetch()) {
1617
                                // Keep sorting of $selectUids
1618
                                $selectedUid = array_search($MMrow['uid'], $selectUids);
1619
                                $mmlA[$selectedUid] = $MMrow['uid'];
1620
                                if (!$noRecordLookup) {
1621
                                    $mmlA[$selectedUid] = static::getRecordTitle(
1622
                                        $theColConf['foreign_table'],
1623
                                        $MMrow,
1624
                                        false,
1625
                                        $forceResult
1626
                                    );
1627
                                }
1628
                            }
1629
1630
                            if (!empty($mmlA)) {
1631
                                ksort($mmlA);
1632
                                $l = implode('; ', $mmlA);
1633
                            } else {
1634
                                $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1635
                            }
1636
                        } else {
1637
                            $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1638
                        }
1639
                    } else {
1640
                        $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1641
                    }
1642
                } else {
1643
                    $columnTsConfig = [];
1644
                    if ($pid) {
1645
                        $pageTsConfig = self::getPagesTSconfig($pid);
1646
                        if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) {
1647
                            $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'];
1648
                        }
1649
                    }
1650
                    $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig);
1651
                    if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) {
1652
                        if ($noRecordLookup) {
1653
                            $l = $value;
1654
                        } else {
1655
                            $rParts = [];
1656
                            if ($uid && isset($theColConf['foreign_field']) && $theColConf['foreign_field'] !== '') {
1657
                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1658
                                $queryBuilder->getRestrictions()
1659
                                    ->removeAll()
1660
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1661
                                    ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace));
1662
                                $constraints = [
1663
                                    $queryBuilder->expr()->eq(
1664
                                        $theColConf['foreign_field'],
1665
                                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1666
                                    )
1667
                                ];
1668
1669
                                if (!empty($theColConf['foreign_table_field'])) {
1670
                                    $constraints[] = $queryBuilder->expr()->eq(
1671
                                        $theColConf['foreign_table_field'],
1672
                                        $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
1673
                                    );
1674
                                }
1675
1676
                                // Add additional where clause if foreign_match_fields are defined
1677
                                $foreignMatchFields = [];
1678
                                if (is_array($theColConf['foreign_match_fields'])) {
1679
                                    $foreignMatchFields = $theColConf['foreign_match_fields'];
1680
                                }
1681
1682
                                foreach ($foreignMatchFields as $matchField => $matchValue) {
1683
                                    $constraints[] = $queryBuilder->expr()->eq(
1684
                                        $matchField,
1685
                                        $queryBuilder->createNamedParameter($matchValue)
1686
                                    );
1687
                                }
1688
1689
                                $result = $queryBuilder
1690
                                    ->select('*')
1691
                                    ->from($theColConf['foreign_table'])
1692
                                    ->where(...$constraints)
1693
                                    ->execute();
1694
1695
                                while ($record = $result->fetch()) {
1696
                                    $rParts[] = $record['uid'];
1697
                                }
1698
                            }
1699
                            if (empty($rParts)) {
1700
                                $rParts = GeneralUtility::trimExplode(',', $value, true);
1701
                            }
1702
                            $lA = [];
1703
                            foreach ($rParts as $rVal) {
1704
                                $rVal = (int)$rVal;
1705
                                $r = self::getRecordWSOL($theColConf['foreign_table'], $rVal);
1706
                                if (is_array($r)) {
1707
                                    $lA[] = $lang->sL($theColConf['foreign_table_prefix'])
1708
                                        . self::getRecordTitle($theColConf['foreign_table'], $r, false, $forceResult);
1709
                                } else {
1710
                                    $lA[] = $rVal ? '[' . $rVal . '!]' : '';
1711
                                }
1712
                            }
1713
                            $l = implode(', ', $lA);
1714
                        }
1715
                    }
1716
                    if (empty($l) && !empty($value)) {
1717
                        // Use plain database value when label is empty
1718
                        $l = $value;
1719
                    }
1720
                }
1721
                break;
1722
            case 'group':
1723
                // resolve the titles for DB records
1724
                if (isset($theColConf['internal_type']) && $theColConf['internal_type'] === 'db') {
1725
                    if (isset($theColConf['MM']) && $theColConf['MM']) {
1726
                        if ($uid) {
1727
                            // Display the title of MM related records in lists
1728
                            if ($noRecordLookup) {
1729
                                $MMfields = [];
1730
                                $MMfields[] = $theColConf['foreign_table'] . '.uid';
1731
                            } else {
1732
                                $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1733
                                $altLabelFields = explode(
1734
                                    ',',
1735
                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt']
1736
                                );
1737
                                foreach ($altLabelFields as $f) {
1738
                                    $f = trim($f);
1739
                                    if ($f !== '') {
1740
                                        $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1741
                                    }
1742
                                }
1743
                            }
1744
                            /** @var RelationHandler $dbGroup */
1745
                            $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1746
                            $dbGroup->start(
1747
                                $value,
1748
                                $theColConf['foreign_table'],
1749
                                $theColConf['MM'],
1750
                                $uid,
1751
                                $table,
1752
                                $theColConf
1753
                            );
1754
                            $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1755
                            if (!empty($selectUids) && is_array($selectUids)) {
1756
                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1757
                                $queryBuilder->getRestrictions()
1758
                                    ->removeAll()
1759
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1760
1761
                                $result = $queryBuilder
1762
                                    ->select('uid', ...$MMfields)
1763
                                    ->from($theColConf['foreign_table'])
1764
                                    ->where(
1765
                                        $queryBuilder->expr()->in(
1766
                                            'uid',
1767
                                            $queryBuilder->createNamedParameter(
1768
                                                $selectUids,
1769
                                                Connection::PARAM_INT_ARRAY
1770
                                            )
1771
                                        )
1772
                                    )
1773
                                    ->execute();
1774
1775
                                $mmlA = [];
1776
                                while ($MMrow = $result->fetch()) {
1777
                                    // Keep sorting of $selectUids
1778
                                    $selectedUid = array_search($MMrow['uid'], $selectUids);
1779
                                    $mmlA[$selectedUid] = $MMrow['uid'];
1780
                                    if (!$noRecordLookup) {
1781
                                        $mmlA[$selectedUid] = static::getRecordTitle(
1782
                                            $theColConf['foreign_table'],
1783
                                            $MMrow,
1784
                                            false,
1785
                                            $forceResult
1786
                                        );
1787
                                    }
1788
                                }
1789
1790
                                if (!empty($mmlA)) {
1791
                                    ksort($mmlA);
1792
                                    $l = implode('; ', $mmlA);
1793
                                } else {
1794
                                    $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1795
                                }
1796
                            } else {
1797
                                $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1798
                            }
1799
                        } else {
1800
                            $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation');
1801
                        }
1802
                    } else {
1803
                        $finalValues = [];
1804
                        $relationTableName = $theColConf['allowed'];
1805
                        $explodedValues = GeneralUtility::trimExplode(',', $value, true);
1806
1807
                        foreach ($explodedValues as $explodedValue) {
1808
                            if (MathUtility::canBeInterpretedAsInteger($explodedValue)) {
1809
                                $relationTableNameForField = $relationTableName;
1810
                            } else {
1811
                                [$relationTableNameForField, $explodedValue] = self::splitTable_Uid($explodedValue);
1812
                            }
1813
1814
                            $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

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

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