Completed
Push — master ( 64542d...27c7de )
by
unknown
12:47
created

BackendUtility::getMovePlaceholder()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 26
nc 4
nop 4
dl 0
loc 38
rs 8.8817
c 0
b 0
f 0

2 Methods

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

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

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

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

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

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

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

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

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

3020
                $pidRec = self::getRecord($table, /** @scrutinizer ignore-type */ abs($thePidValue), 'pid');
Loading history...
3021
                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
3022
            }
3023
        } else {
3024
            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
3025
            $rr = self::getRecord($table, $uid);
3026
            $thePidValue = null;
3027
            if (is_array($rr)) {
3028
                // First check if the pid is -1 which means it is a workspaced element. Get the "real" record:
3029
                if ($rr['pid'] == '-1') {
3030
                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
3031
                    if (is_array($rr)) {
3032
                        $thePidValue = $rr['pid'];
3033
                    }
3034
                } else {
3035
                    // Returning the "pid" of the record
3036
                    $thePidValue = $rr['pid'];
3037
                }
3038
            }
3039
            if (!$thePidValue) {
3040
                // Returns -1 if the record with this pid was not found.
3041
                $thePidValue = -1;
3042
            }
3043
        }
3044
        return $thePidValue;
3045
    }
3046
3047
    /**
3048
     * Return the real pid of a record and caches the result.
3049
     * The non-cached method needs database queries to do the job, so this method
3050
     * can be used if code sometimes calls the same record multiple times to save
3051
     * some queries. This should not be done if the calling code may change the
3052
     * same record meanwhile.
3053
     *
3054
     * @param string $table Tablename
3055
     * @param string $uid UID value
3056
     * @param string $pid PID value
3057
     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
3058
     */
3059
    public static function getTSCpidCached($table, $uid, $pid)
3060
    {
3061
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3062
        $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: [];
3063
        $key = $table . ':' . $uid . ':' . $pid;
3064
        if (!isset($firstLevelCache[$key])) {
3065
            $firstLevelCache[$key] = static::getTSCpid($table, $uid, $pid);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

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

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

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

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