Completed
Push — master ( 871340...ff990f )
by
unknown
31:24 queued 14:17
created

BackendUtility::purgeComputedPropertyNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

2064
                    $fVnew = self::getRecordPath(/** @scrutinizer ignore-type */ $fV, '1=1', 20);
Loading history...
2065
                } else {
2066
                    $fVnew = $fV;
2067
                }
2068
            }
2069
        }
2070
        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...
2071
    }
2072
2073
    /**
2074
     * Returns fields for a table, $table, which would typically be interesting to select
2075
     * This includes uid, the fields defined for title, icon-field.
2076
     * 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)
2077
     *
2078
     * @param string $table Table name, present in $GLOBALS['TCA']
2079
     * @param string $prefix Table prefix
2080
     * @param array $fields Preset fields (must include prefix if that is used)
2081
     * @return string List of fields.
2082
     * @internal should only be used from within TYPO3 Core
2083
     */
2084
    public static function getCommonSelectFields($table, $prefix = '', $fields = [])
2085
    {
2086
        $fields[] = $prefix . 'uid';
2087
        if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
2088
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
2089
        }
2090
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) {
2091
            $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
2092
            foreach ($secondFields as $fieldN) {
2093
                $fields[] = $prefix . $fieldN;
2094
            }
2095
        }
2096
        if (static::isTableWorkspaceEnabled($table)) {
2097
            $fields[] = $prefix . 't3ver_state';
2098
            $fields[] = $prefix . 't3ver_wsid';
2099
            $fields[] = $prefix . 't3ver_count';
2100
        }
2101
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) {
2102
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
2103
        }
2104
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
2105
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
2106
        }
2107
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
2108
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2109
        }
2110
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) {
2111
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
2112
        }
2113
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) {
2114
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
2115
        }
2116
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
2117
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
2118
        }
2119
        return implode(',', array_unique($fields));
2120
    }
2121
2122
    /*******************************************
2123
     *
2124
     * Backend Modules API functions
2125
     *
2126
     *******************************************/
2127
2128
    /**
2129
     * Returns CSH help text (description), if configured for, as an array (title, description)
2130
     *
2131
     * @param string $table Table name
2132
     * @param string $field Field name
2133
     * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
2134
     * @internal should only be used from within TYPO3 Core
2135
     */
2136
    public static function helpTextArray($table, $field)
2137
    {
2138
        if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
2139
            static::getLanguageService()->loadSingleTableDescription($table);
2140
        }
2141
        $output = [
2142
            'description' => null,
2143
            'title' => null,
2144
            'moreInfo' => false
2145
        ];
2146
        if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
2147
            $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
2148
            // Add alternative title, if defined
2149
            if ($data['alttitle']) {
2150
                $output['title'] = $data['alttitle'];
2151
            }
2152
            // If we have more information to show and access to the cshmanual
2153
            if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax'])
2154
                && static::getBackendUserAuthentication()->check('modules', 'help_CshmanualCshmanual')
2155
            ) {
2156
                $output['moreInfo'] = true;
2157
            }
2158
            // Add description
2159
            if ($data['description']) {
2160
                $output['description'] = $data['description'];
2161
            }
2162
        }
2163
        return $output;
2164
    }
2165
2166
    /**
2167
     * Returns CSH help text
2168
     *
2169
     * @param string $table Table name
2170
     * @param string $field Field name
2171
     * @return string HTML content for help text
2172
     * @see cshItem()
2173
     * @internal should only be used from within TYPO3 Core
2174
     */
2175
    public static function helpText($table, $field)
2176
    {
2177
        $helpTextArray = self::helpTextArray($table, $field);
2178
        $output = '';
2179
        $arrow = '';
2180
        // Put header before the rest of the text
2181
        if ($helpTextArray['title'] !== null) {
2182
            $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
2183
        }
2184
        // Add see also arrow if we have more info
2185
        if ($helpTextArray['moreInfo']) {
2186
            /** @var IconFactory $iconFactory */
2187
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2188
            $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
2189
        }
2190
        // Wrap description and arrow in p tag
2191
        if ($helpTextArray['description'] !== null || $arrow) {
2192
            $output .= '<p class="help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>';
2193
        }
2194
        return $output;
2195
    }
2196
2197
    /**
2198
     * API function that wraps the text / html in help text, so if a user hovers over it
2199
     * the help text will show up
2200
     *
2201
     * @param string $table The table name for which the help should be shown
2202
     * @param string $field The field name for which the help should be shown
2203
     * @param string $text The text which should be wrapped with the help text
2204
     * @param array $overloadHelpText Array with text to overload help text
2205
     * @return string the HTML code ready to render
2206
     * @internal should only be used from within TYPO3 Core
2207
     */
2208
    public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
2209
    {
2210
        // Initialize some variables
2211
        $helpText = '';
2212
        $abbrClassAdd = '';
2213
        $hasHelpTextOverload = !empty($overloadHelpText);
2214
        // Get the help text that should be shown on hover
2215
        if (!$hasHelpTextOverload) {
2216
            $helpText = self::helpText($table, $field);
2217
        }
2218
        // If there's a help text or some overload information, proceed with preparing an output
2219
        if (!empty($helpText) || $hasHelpTextOverload) {
2220
            // If no text was given, just use the regular help icon
2221
            if ($text == '') {
2222
                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2223
                $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2224
                $abbrClassAdd = ' help-teaser-icon';
2225
            }
2226
            $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2227
            $wrappedText = '<span class="help-link" href="#" data-table="' . $table . '" data-field="' . $field . '"';
2228
            // The overload array may provide a title and a description
2229
            // If either one is defined, add them to the "data" attributes
2230
            if ($hasHelpTextOverload) {
2231
                if (isset($overloadHelpText['title'])) {
2232
                    $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2233
                }
2234
                if (isset($overloadHelpText['description'])) {
2235
                    $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2236
                }
2237
            }
2238
            $wrappedText .= '>' . $text . '</span>';
2239
            return $wrappedText;
2240
        }
2241
        return $text;
2242
    }
2243
2244
    /**
2245
     * API for getting CSH icons/text for use in backend modules.
2246
     * TCA_DESCR will be loaded if it isn't already
2247
     *
2248
     * @param string $table Table name ('_MOD_'+module name)
2249
     * @param string $field Field name (CSH locallang main key)
2250
     * @param string $_ (unused)
2251
     * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2252
     * @return string HTML content for help text
2253
     */
2254
    public static function cshItem($table, $field, $_ = '', $wrap = '')
2255
    {
2256
        static::getLanguageService()->loadSingleTableDescription($table);
2257
        if (is_array($GLOBALS['TCA_DESCR'][$table])
2258
            && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])
2259
        ) {
2260
            // Creating short description
2261
            $output = self::wrapInHelp($table, $field);
2262
            if ($output && $wrap) {
2263
                $wrParts = explode('|', $wrap);
2264
                $output = $wrParts[0] . $output . $wrParts[1];
2265
            }
2266
            return $output;
2267
        }
2268
        return '';
2269
    }
2270
2271
    /**
2272
     * Returns a JavaScript string for viewing the page id, $id
2273
     * It will re-use any window already open.
2274
     *
2275
     * @param int $pageUid Page UID
2276
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2277
     * @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)
2278
     * @param string $anchorSection Optional anchor to the URL
2279
     * @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!
2280
     * @param string $additionalGetVars Additional GET variables.
2281
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2282
     * @return string
2283
     */
2284
    public static function viewOnClick(
2285
        $pageUid,
2286
        $backPath = '',
2287
        $rootLine = null,
2288
        $anchorSection = '',
2289
        $alternativeUrl = '',
2290
        $additionalGetVars = '',
2291
        $switchFocus = true
2292
    ) {
2293
        try {
2294
            $previewUrl = self::getPreviewUrl(
2295
                $pageUid,
2296
                $backPath,
2297
                $rootLine,
2298
                $anchorSection,
2299
                $alternativeUrl,
2300
                $additionalGetVars,
2301
                $switchFocus
2302
            );
2303
        } catch (UnableToLinkToPageException $e) {
2304
            return '';
2305
        }
2306
2307
        $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');'
2308
            . ($switchFocus ? 'previewWin.focus();' : '') . LF
2309
            . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };';
2310
2311
        return $onclickCode;
2312
    }
2313
2314
    /**
2315
     * Returns the preview url
2316
     *
2317
     * It will detect the correct domain name if needed and provide the link with the right back path.
2318
     *
2319
     * @param int $pageUid Page UID
2320
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2321
     * @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)
2322
     * @param string $anchorSection Optional anchor to the URL
2323
     * @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!
2324
     * @param string $additionalGetVars Additional GET variables.
2325
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2326
     * @return string
2327
     */
2328
    public static function getPreviewUrl(
2329
        $pageUid,
2330
        $backPath = '',
2331
        $rootLine = null,
2332
        $anchorSection = '',
2333
        $alternativeUrl = '',
2334
        $additionalGetVars = '',
2335
        &$switchFocus = true
2336
    ): string {
2337
        $viewScript = '/index.php?id=';
2338
        if ($alternativeUrl) {
2339
            $viewScript = $alternativeUrl;
2340
        }
2341
2342
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2343
            $hookObj = GeneralUtility::makeInstance($className);
2344
            if (method_exists($hookObj, 'preProcess')) {
2345
                $hookObj->preProcess(
2346
                    $pageUid,
2347
                    $backPath,
2348
                    $rootLine,
2349
                    $anchorSection,
2350
                    $viewScript,
2351
                    $additionalGetVars,
2352
                    $switchFocus
2353
                );
2354
            }
2355
        }
2356
2357
        // If there is an alternative URL or the URL has been modified by a hook, use that one.
2358
        if ($alternativeUrl || $viewScript !== '/index.php?id=') {
2359
            $previewUrl = $viewScript;
2360
        } else {
2361
            $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2362
            $pageInfo = self::readPageAccess($pageUid, $permissionClause);
2363
            // prepare custom context for link generation (to allow for example time based previews)
2364
            $context = clone GeneralUtility::makeInstance(Context::class);
2365
            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context);
2366
2367
            // Build the URL with a site as prefix, if configured
2368
            $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
2369
            // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is
2370
            $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2371
            try {
2372
                $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine);
2373
            } catch (SiteNotFoundException $e) {
2374
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794919);
2375
            }
2376
            // Create a multi-dimensional array out of the additional get vars
2377
            $additionalQueryParams = [];
2378
            parse_str($additionalGetVars, $additionalQueryParams);
2379
            if (isset($additionalQueryParams['L'])) {
2380
                $additionalQueryParams['_language'] = $additionalQueryParams['_language'] ?? $additionalQueryParams['L'];
2381
                unset($additionalQueryParams['L']);
2382
            }
2383
            try {
2384
                $previewUrl = (string)$site->getRouter($context)->generateUri(
2385
                    $pageUid,
2386
                    $additionalQueryParams,
2387
                    $anchorSection,
2388
                    RouterInterface::ABSOLUTE_URL
2389
                );
2390
            } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
2391
                throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794914);
2392
            }
2393
        }
2394
2395
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2396
            $hookObj = GeneralUtility::makeInstance($className);
2397
            if (method_exists($hookObj, 'postProcess')) {
2398
                $previewUrl = $hookObj->postProcess(
2399
                    $previewUrl,
2400
                    $pageUid,
2401
                    $rootLine,
2402
                    $anchorSection,
2403
                    $viewScript,
2404
                    $additionalGetVars,
2405
                    $switchFocus
2406
                );
2407
            }
2408
        }
2409
2410
        return $previewUrl;
2411
    }
2412
2413
    /**
2414
     * Makes click menu link (context sensitive menu)
2415
     *
2416
     * Returns $str wrapped in a link which will activate the context sensitive
2417
     * menu for the record ($table/$uid) or file ($table = file)
2418
     * The link will load the top frame with the parameter "&item" which is the table, uid
2419
     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2420
     *
2421
     * @param string $content String to be wrapped in link, typ. image tag.
2422
     * @param string $table Table name/File path. If the icon is for a database
2423
     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2424
     * the absolute filepath
2425
     * @param int|string $uid If icon is for database record this is the UID for the
2426
     * record from $table or identifier for sys_file record
2427
     * @param string $context Set tree if menu is called from tree view
2428
     * @param string $_addParams NOT IN USE
2429
     * @param string $_enDisItems NOT IN USE
2430
     * @param bool $returnTagParameters If set, will return only the onclick
2431
     * JavaScript, not the whole link.
2432
     *
2433
     * @return string The link wrapped input string.
2434
     */
2435
    public static function wrapClickMenuOnIcon(
2436
        $content,
2437
        $table,
2438
        $uid = 0,
2439
        $context = '',
2440
        $_addParams = '',
2441
        $_enDisItems = '',
2442
        $returnTagParameters = false
2443
    ) {
2444
        $tagParameters = [
2445
            'class' => 't3js-contextmenutrigger',
2446
            'data-table' => $table,
2447
            'data-uid' => $uid,
2448
            'data-context' => $context
2449
        ];
2450
2451
        if ($returnTagParameters) {
2452
            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...
2453
        }
2454
        return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2455
    }
2456
2457
    /**
2458
     * Returns a URL with a command to TYPO3 Datahandler
2459
     *
2460
     * @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
2461
     * @param string $redirectUrl Redirect URL, default is to use GeneralUtility::getIndpEnv('REQUEST_URI')
2462
     * @return string
2463
     */
2464
    public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2465
    {
2466
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2467
        $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2468
        $url .= rawurlencode($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI'));
2469
        return $url;
2470
    }
2471
2472
    /**
2473
     * Returns a selector box "function menu" for a module
2474
     * See Inside TYPO3 for details about how to use / make Function menus
2475
     *
2476
     * @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=...
2477
     * @param string $elementName The form elements name, probably something like "SET[...]
2478
     * @param string $currentValue The value to be selected currently.
2479
     * @param array $menuItems An array with the menu items for the selector box
2480
     * @param string $script The script to send the &id to, if empty it's automatically found
2481
     * @param string $addParams Additional parameters to pass to the script.
2482
     * @return string HTML code for selector box
2483
     */
2484
    public static function getFuncMenu(
2485
        $mainParams,
2486
        $elementName,
2487
        $currentValue,
2488
        $menuItems,
2489
        $script = '',
2490
        $addParams = ''
2491
    ) {
2492
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2493
            return '';
2494
        }
2495
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2496
        $options = [];
2497
        foreach ($menuItems as $value => $label) {
2498
            $options[] = '<option value="'
2499
                . htmlspecialchars($value) . '"'
2500
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2501
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2502
        }
2503
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2504
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2505
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2506
        if (!empty($options)) {
2507
            // @todo Should we add requireJsModule again (should be loaded in most/all cases)
2508
            // loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
2509
            $attributes = GeneralUtility::implodeAttributes([
2510
                'name' => $elementName,
2511
                'class' => 'form-control',
2512
                'data-menu-identifier' => $dataMenuIdentifier,
2513
                'data-global-event' => 'change',
2514
                'data-action-navigate' => '$data=~s/$value/',
2515
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2516
            ], true);
2517
            return sprintf(
2518
                '<select %s>%s</select>',
2519
                $attributes,
2520
                implode('', $options)
2521
            );
2522
        }
2523
        return '';
2524
    }
2525
2526
    /**
2527
     * Returns a selector box to switch the view
2528
     * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose.
2529
     * Mingling with getFuncMenu would harm the docHeader Menu.
2530
     *
2531
     * @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=...
2532
     * @param string $elementName The form elements name, probably something like "SET[...]
2533
     * @param string $currentValue The value to be selected currently.
2534
     * @param array $menuItems An array with the menu items for the selector box
2535
     * @param string $script The script to send the &id to, if empty it's automatically found
2536
     * @param string $addParams Additional parameters to pass to the script.
2537
     * @return string HTML code for selector box
2538
     */
2539
    public static function getDropdownMenu(
2540
        $mainParams,
2541
        $elementName,
2542
        $currentValue,
2543
        $menuItems,
2544
        $script = '',
2545
        $addParams = ''
2546
    ) {
2547
        if (!is_array($menuItems) || count($menuItems) <= 1) {
0 ignored issues
show
introduced by
The condition is_array($menuItems) is always true.
Loading history...
2548
            return '';
2549
        }
2550
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2551
        $options = [];
2552
        foreach ($menuItems as $value => $label) {
2553
            $options[] = '<option value="'
2554
                . htmlspecialchars($value) . '"'
2555
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2556
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2557
        }
2558
        $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName);
2559
        $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier);
2560
        $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier);
2561
        if (!empty($options)) {
2562
            // @todo Should we add requireJsModule again (should be loaded in most/all cases)
2563
            // loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
2564
            $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+this.options[this.selectedIndex].value;';
0 ignored issues
show
Unused Code introduced by
The assignment to $onChange is dead and can be removed.
Loading history...
2565
            $attributes = GeneralUtility::implodeAttributes([
2566
                'name' => $elementName,
2567
                'data-menu-identifier' => $dataMenuIdentifier,
2568
                'data-global-event' => 'change',
2569
                'data-action-navigate' => '$data=~s/$value/',
2570
                'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}',
2571
            ], true);
2572
            return '
2573
			<div class="form-group">
2574
				<!-- Function Menu of module -->
2575
				<select class="form-control input-sm" ' . $attributes . '>
2576
					' . implode(LF, $options) . '
2577
				</select>
2578
			</div>
2579
						';
2580
        }
2581
        return '';
2582
    }
2583
2584
    /**
2585
     * Checkbox function menu.
2586
     * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox.
2587
     *
2588
     * @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=...
2589
     * @param string $elementName The form elements name, probably something like "SET[...]
2590
     * @param string $currentValue The value to be selected currently.
2591
     * @param string $script The script to send the &id to, if empty it's automatically found
2592
     * @param string $addParams Additional parameters to pass to the script.
2593
     * @param string $tagParams Additional attributes for the checkbox input tag
2594
     * @return string HTML code for checkbox
2595
     * @see getFuncMenu()
2596
     */
2597
    public static function getFuncCheck(
2598
        $mainParams,
2599
        $elementName,
2600
        $currentValue,
2601
        $script = '',
2602
        $addParams = '',
2603
        $tagParams = ''
2604
    ) {
2605
        // @todo Should we add requireJsModule again (should be loaded in most/all cases)
2606
        // loadRequireJsModule('TYPO3/CMS/Backend/GlobalEventHandler');
2607
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2608
        $attributes = GeneralUtility::implodeAttributes([
2609
            'type' => 'checkbox',
2610
            'class' => 'checkbox',
2611
            'name' => $elementName,
2612
            'value' => 1,
2613
            'data-global-event' => 'change',
2614
            'data-action-navigate' => '$data=~s/$value/',
2615
            'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName),
2616
        ], true);
2617
        return
2618
            '<input ' . $attributes .
2619
            ($currentValue ? ' checked="checked"' : '') .
2620
            ($tagParams ? ' ' . $tagParams : '') .
2621
            ' />';
2622
    }
2623
2624
    /**
2625
     * Input field function menu
2626
     * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange"
2627
     *
2628
     * @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=...
2629
     * @param string $elementName The form elements name, probably something like "SET[...]
2630
     * @param string $currentValue The value to be selected currently.
2631
     * @param int $size Relative size of input field, max is 48
2632
     * @param string $script The script to send the &id to, if empty it's automatically found
2633
     * @param string $addParams Additional parameters to pass to the script.
2634
     * @return string HTML code for input text field.
2635
     * @see getFuncMenu()
2636
     */
2637
    public static function getFuncInput(
2638
        $mainParams,
2639
        $elementName,
2640
        $currentValue,
2641
        $size = 10,
2642
        $script = '',
2643
        $addParams = ''
2644
    ) {
2645
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2646
        $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value);';
2647
        return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />';
2648
    }
2649
2650
    /**
2651
     * Builds the URL to the current script with given arguments
2652
     *
2653
     * @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=...
2654
     * @param string $addParams Additional parameters to pass to the script.
2655
     * @param string $script The script to send the &id to, if empty it's automatically found
2656
     * @return string The complete script URL
2657
     */
2658
    protected static function buildScriptUrl($mainParams, $addParams, $script = '')
2659
    {
2660
        if (!is_array($mainParams)) {
2661
            $mainParams = ['id' => $mainParams];
2662
        }
2663
        if (!$script) {
2664
            $script = PathUtility::basename(Environment::getCurrentScript());
2665
        }
2666
2667
        if ($routePath = GeneralUtility::_GP('route')) {
2668
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2669
            $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
2670
            $scriptUrl .= $addParams;
2671
        } else {
2672
            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
2673
        }
2674
2675
        return $scriptUrl;
2676
    }
2677
2678
    /**
2679
     * Call to update the page tree frame (or something else..?) after
2680
     * use 'updatePageTree' as a first parameter will set the page tree to be updated.
2681
     *
2682
     * @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.
2683
     * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
2684
     * @see BackendUtility::getUpdateSignalCode()
2685
     */
2686
    public static function setUpdateSignal($set = '', $params = '')
2687
    {
2688
        $beUser = static::getBackendUserAuthentication();
2689
        $modData = $beUser->getModuleData(
2690
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2691
            'ses'
2692
        );
2693
        if ($set) {
2694
            $modData[$set] = [
2695
                'set' => $set,
2696
                'parameter' => $params
2697
            ];
2698
        } else {
2699
            // clear the module data
2700
            $modData = [];
2701
        }
2702
        $beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2703
    }
2704
2705
    /**
2706
     * Call to update the page tree frame (or something else..?) if this is set by the function
2707
     * setUpdateSignal(). It will return some JavaScript that does the update
2708
     *
2709
     * @return string HTML javascript code
2710
     * @see BackendUtility::setUpdateSignal()
2711
     */
2712
    public static function getUpdateSignalCode()
2713
    {
2714
        $signals = [];
2715
        $modData = static::getBackendUserAuthentication()->getModuleData(
2716
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2717
            'ses'
2718
        );
2719
        if (empty($modData)) {
2720
            return '';
2721
        }
2722
        // Hook: Allows to let TYPO3 execute your JS code
2723
        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
2724
        // Loop through all setUpdateSignals and get the JS code
2725
        foreach ($modData as $set => $val) {
2726
            if (isset($updateSignals[$set])) {
2727
                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => ''];
2728
                $ref = null;
2729
                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
2730
                $signals[] = $params['JScode'];
2731
            } else {
2732
                switch ($set) {
2733
                    case 'updatePageTree':
2734
                        $signals[] = '
2735
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) {
2736
									top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
2737
								}
2738
							';
2739
                        break;
2740
                    case 'updateFolderTree':
2741
                        $signals[] = '
2742
								if (top && top.nav_frame && top.nav_frame.location) {
2743
									top.nav_frame.location.reload(true);
2744
								}';
2745
                        break;
2746
                    case 'updateModuleMenu':
2747
                        $signals[] = '
2748
								if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) {
2749
									top.TYPO3.ModuleMenu.App.refreshMenu();
2750
								}';
2751
                        break;
2752
                    case 'updateTopbar':
2753
                        $signals[] = '
2754
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) {
2755
									top.TYPO3.Backend.Topbar.refresh();
2756
								}';
2757
                        break;
2758
                }
2759
            }
2760
        }
2761
        $content = implode(LF, $signals);
2762
        // For backwards compatibility, should be replaced
2763
        self::setUpdateSignal();
2764
        return $content;
2765
    }
2766
2767
    /**
2768
     * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
2769
     * This is kind of session variable management framework for the backend users.
2770
     * 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
2771
     * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules.
2772
     *
2773
     * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus.
2774
     * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus.
2775
     * @param string $modName modName is the name of this module. Used to get the correct module data.
2776
     * @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.
2777
     * @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.
2778
     * @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)
2779
     * @throws \RuntimeException
2780
     * @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
2781
     */
2782
    public static function getModuleData(
2783
        $MOD_MENU,
2784
        $CHANGED_SETTINGS,
2785
        $modName,
2786
        $type = '',
2787
        $dontValidateList = '',
2788
        $setDefaultList = ''
2789
    ) {
2790
        if ($modName && is_string($modName)) {
2791
            // Getting stored user-data from this module:
2792
            $beUser = static::getBackendUserAuthentication();
2793
            $settings = $beUser->getModuleData($modName, $type);
2794
            $changed = 0;
2795
            if (!is_array($settings)) {
2796
                $changed = 1;
2797
                $settings = [];
2798
            }
2799
            if (is_array($MOD_MENU)) {
0 ignored issues
show
introduced by
The condition is_array($MOD_MENU) is always true.
Loading history...
2800
                foreach ($MOD_MENU as $key => $var) {
2801
                    // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array.
2802
                    if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) {
2803
                        if (is_array($CHANGED_SETTINGS[$key])) {
2804
                            $serializedSettings = serialize($CHANGED_SETTINGS[$key]);
2805
                            if ((string)$settings[$key] !== $serializedSettings) {
2806
                                $settings[$key] = $serializedSettings;
2807
                                $changed = 1;
2808
                            }
2809
                        } else {
2810
                            if ((string)$settings[$key] !== (string)$CHANGED_SETTINGS[$key]) {
2811
                                $settings[$key] = $CHANGED_SETTINGS[$key];
2812
                                $changed = 1;
2813
                            }
2814
                        }
2815
                    }
2816
                    // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted
2817
                    if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) {
2818
                        // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted.
2819
                        if (is_array($settings[$key]) || !isset($MOD_MENU[$key][$settings[$key]])) {
2820
                            $settings[$key] = (string)key($var);
2821
                            $changed = 1;
2822
                        }
2823
                    }
2824
                    // Sets default values (only strings/checkboxes, not menus)
2825
                    if ($setDefaultList && !is_array($var)) {
2826
                        if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) {
2827
                            $settings[$key] = (string)$var;
2828
                        }
2829
                    }
2830
                }
2831
            } else {
2832
                throw new \RuntimeException('No menu', 1568119229);
2833
            }
2834
            if ($changed) {
2835
                $beUser->pushModuleData($modName, $settings);
2836
            }
2837
            return $settings;
2838
        }
2839
        throw new \RuntimeException('Wrong module name "' . $modName . '"', 1568119221);
2840
    }
2841
2842
    /*******************************************
2843
     *
2844
     * Core
2845
     *
2846
     *******************************************/
2847
    /**
2848
     * Unlock or Lock a record from $table with $uid
2849
     * If $table and $uid is not set, then all locking for the current BE_USER is removed!
2850
     *
2851
     * @param string $table Table name
2852
     * @param int $uid Record uid
2853
     * @param int $pid Record pid
2854
     * @internal
2855
     */
2856
    public static function lockRecords($table = '', $uid = 0, $pid = 0)
2857
    {
2858
        $beUser = static::getBackendUserAuthentication();
2859
        if (isset($beUser->user['uid'])) {
2860
            $userId = (int)$beUser->user['uid'];
2861
            if ($table && $uid) {
2862
                $fieldsValues = [
2863
                    'userid' => $userId,
2864
                    'feuserid' => 0,
2865
                    'tstamp' => $GLOBALS['EXEC_TIME'],
2866
                    'record_table' => $table,
2867
                    'record_uid' => $uid,
2868
                    'username' => $beUser->user['username'],
2869
                    'record_pid' => $pid
2870
                ];
2871
                GeneralUtility::makeInstance(ConnectionPool::class)
2872
                    ->getConnectionForTable('sys_lockedrecords')
2873
                    ->insert(
2874
                        'sys_lockedrecords',
2875
                        $fieldsValues
2876
                    );
2877
            } else {
2878
                GeneralUtility::makeInstance(ConnectionPool::class)
2879
                    ->getConnectionForTable('sys_lockedrecords')
2880
                    ->delete(
2881
                        'sys_lockedrecords',
2882
                        ['userid' => (int)$userId]
2883
                    );
2884
            }
2885
        }
2886
    }
2887
2888
    /**
2889
     * Returns information about whether the record from table, $table, with uid, $uid is currently locked
2890
     * (edited by another user - which should issue a warning).
2891
     * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts
2892
     * are activated - which means that a user CAN have a record "open" without having it locked.
2893
     * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient.
2894
     *
2895
     * @param string $table Table name
2896
     * @param int $uid Record uid
2897
     * @return array|bool
2898
     * @internal
2899
     */
2900
    public static function isRecordLocked($table, $uid)
2901
    {
2902
        $runtimeCache = self::getRuntimeCache();
2903
        $cacheId = 'backend-recordLocked';
2904
        $recordLockedCache = $runtimeCache->get($cacheId);
2905
        if ($recordLockedCache !== false) {
2906
            $lockedRecords = $recordLockedCache;
2907
        } else {
2908
            $lockedRecords = [];
2909
2910
            $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords');
2911
            $result = $queryBuilder
2912
                ->select('*')
2913
                ->from('sys_lockedrecords')
2914
                ->where(
2915
                    $queryBuilder->expr()->neq(
2916
                        'sys_lockedrecords.userid',
2917
                        $queryBuilder->createNamedParameter(
2918
                            static::getBackendUserAuthentication()->user['uid'],
2919
                            \PDO::PARAM_INT
2920
                        )
2921
                    ),
2922
                    $queryBuilder->expr()->gt(
2923
                        'sys_lockedrecords.tstamp',
2924
                        $queryBuilder->createNamedParameter(
2925
                            $GLOBALS['EXEC_TIME'] - 2 * 3600,
2926
                            \PDO::PARAM_INT
2927
                        )
2928
                    )
2929
                )
2930
                ->execute();
2931
2932
            $lang = static::getLanguageService();
2933
            while ($row = $result->fetch()) {
2934
                // Get the type of the user that locked this record:
2935
                if ($row['userid']) {
2936
                    $userTypeLabel = 'beUser';
2937
                } elseif ($row['feuserid']) {
2938
                    $userTypeLabel = 'feUser';
2939
                } else {
2940
                    $userTypeLabel = 'user';
2941
                }
2942
                $userType = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel);
2943
                // Get the username (if available):
2944
                if ($row['username']) {
2945
                    $userName = $row['username'];
2946
                } else {
2947
                    $userName = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.unknownUser');
2948
                }
2949
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row;
2950
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf(
2951
                    $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'),
2952
                    $userType,
2953
                    $userName,
2954
                    self::calcAge(
2955
                        $GLOBALS['EXEC_TIME'] - $row['tstamp'],
2956
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2957
                    )
2958
                );
2959
                if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) {
2960
                    $lockedRecords['pages:' . $row['record_pid']]['msg'] = sprintf(
2961
                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'),
2962
                        $userType,
2963
                        $userName,
2964
                        self::calcAge(
2965
                            $GLOBALS['EXEC_TIME'] - $row['tstamp'],
2966
                            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2967
                        )
2968
                    );
2969
                }
2970
            }
2971
            $runtimeCache->set($cacheId, $lockedRecords);
2972
        }
2973
2974
        return $lockedRecords[$table . ':' . $uid] ?? false;
2975
    }
2976
2977
    /**
2978
     * Returns TSConfig for the TCEFORM object in Page TSconfig.
2979
     * Used in TCEFORMs
2980
     *
2981
     * @param string $table Table name present in TCA
2982
     * @param array $row Row from table
2983
     * @return array
2984
     */
2985
    public static function getTCEFORM_TSconfig($table, $row)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::getTCEFORM_TSconfig" is not in camel caps format
Loading history...
2986
    {
2987
        self::fixVersioningPid($table, $row);
2988
        $res = [];
2989
        // Get main config for the table
2990
        [$TScID, $cPid] = self::getTSCpid($table, $row['uid'], $row['pid']);
2991
        if ($TScID >= 0) {
2992
            $tsConfig = static::getPagesTSconfig($TScID)['TCEFORM.'][$table . '.'] ?? [];
2993
            $typeVal = self::getTCAtypeValue($table, $row);
2994
            foreach ($tsConfig as $key => $val) {
2995
                if (is_array($val)) {
2996
                    $fieldN = substr($key, 0, -1);
2997
                    $res[$fieldN] = $val;
2998
                    unset($res[$fieldN]['types.']);
2999
                    if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'])) {
3000
                        ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']);
3001
                    }
3002
                }
3003
            }
3004
        }
3005
        $res['_CURRENT_PID'] = $cPid;
3006
        $res['_THIS_UID'] = $row['uid'];
3007
        // So the row will be passed to foreign_table_where_query()
3008
        $res['_THIS_ROW'] = $row;
3009
        return $res;
3010
    }
3011
3012
    /**
3013
     * Find the real PID of the record (with $uid from $table).
3014
     * 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).
3015
     * NOTICE: Make sure that the input PID is never negative because the record was an offline version!
3016
     * Therefore, you should always use BackendUtility::fixVersioningPid($table,$row); on the data you input before calling this function!
3017
     *
3018
     * @param string $table Table name
3019
     * @param int $uid Record uid
3020
     * @param int $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return
3021
     * @return int
3022
     * @internal
3023
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord()
3024
     * @see \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid()
3025
     */
3026
    public static function getTSconfig_pidValue($table, $uid, $pid)
0 ignored issues
show
Coding Style introduced by
Method name "BackendUtility::getTSconfig_pidValue" is not in camel caps format
Loading history...
3027
    {
3028
        // If pid is an integer this takes precedence in our lookup.
3029
        if (MathUtility::canBeInterpretedAsInteger($pid)) {
3030
            $thePidValue = (int)$pid;
3031
            // If ref to another record, look that record up.
3032
            if ($thePidValue < 0) {
3033
                $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

3033
                $pidRec = self::getRecord($table, /** @scrutinizer ignore-type */ abs($thePidValue), 'pid');
Loading history...
3034
                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
3035
            }
3036
        } else {
3037
            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
3038
            $rr = self::getRecord($table, $uid);
3039
            $thePidValue = null;
3040
            if (is_array($rr)) {
3041
                // First check if the pid is -1 which means it is a workspaced element. Get the "real" record:
3042
                if ($rr['pid'] == '-1') {
3043
                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
3044
                    if (is_array($rr)) {
3045
                        $thePidValue = $rr['pid'];
3046
                    }
3047
                } else {
3048
                    // Returning the "pid" of the record
3049
                    $thePidValue = $rr['pid'];
3050
                }
3051
            }
3052
            if (!$thePidValue) {
3053
                // Returns -1 if the record with this pid was not found.
3054
                $thePidValue = -1;
3055
            }
3056
        }
3057
        return $thePidValue;
3058
    }
3059
3060
    /**
3061
     * Return the real pid of a record and caches the result.
3062
     * The non-cached method needs database queries to do the job, so this method
3063
     * can be used if code sometimes calls the same record multiple times to save
3064
     * some queries. This should not be done if the calling code may change the
3065
     * same record meanwhile.
3066
     *
3067
     * @param string $table Tablename
3068
     * @param string $uid UID value
3069
     * @param string $pid PID value
3070
     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
3071
     */
3072
    public static function getTSCpidCached($table, $uid, $pid)
3073
    {
3074
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3075
        $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: [];
3076
        $key = $table . ':' . $uid . ':' . $pid;
3077
        if (!isset($firstLevelCache[$key])) {
3078
            $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

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

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