Passed
Branch master (6c65a4)
by Christian
16:31
created

BackendUtility::getHash()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

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

1285
        /** @scrutinizer ignore-unused */ $backPath = '',

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1286
        $thumbScript = '',
0 ignored issues
show
Unused Code introduced by
The parameter $thumbScript is not used and could be removed. ( Ignorable by Annotation )

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

1286
        /** @scrutinizer ignore-unused */ $thumbScript = '',

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1287
        $uploaddir = null,
1288
        $abs = 0,
0 ignored issues
show
Unused Code introduced by
The parameter $abs is not used and could be removed. ( Ignorable by Annotation )

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

1288
        /** @scrutinizer ignore-unused */ $abs = 0,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1289
        $tparams = '',
1290
        $size = '',
1291
        $linkInfoPopup = true
1292
    ) {
1293
        // Check and parse the size parameter
1294
        $size = trim($size);
1295
        $sizeParts = [64, 64];
1296
        if ($size) {
1297
            $sizeParts = explode('x', $size . 'x' . $size);
1298
        }
1299
        $thumbData = '';
1300
        $fileReferences = static::resolveFileReferences($table, $field, $row);
1301
        // FAL references
1302
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1303
        if ($fileReferences !== null) {
1304
            foreach ($fileReferences as $fileReferenceObject) {
1305
                // Do not show previews of hidden references
1306
                if ($fileReferenceObject->getProperty('hidden')) {
1307
                    continue;
1308
                }
1309
                $fileObject = $fileReferenceObject->getOriginalFile();
1310
1311
                if ($fileObject->isMissing()) {
1312
                    $thumbData .= '<span class="label label-danger">'
1313
                        . htmlspecialchars(
1314
                            static::getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_missing')
1315
                        )
1316
                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1317
                    continue;
1318
                }
1319
1320
                // Preview web image or media elements
1321
                if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
1322
                    && GeneralUtility::inList(
1323
                        $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
1324
                        $fileReferenceObject->getExtension()
1325
                    )
1326
                ) {
1327
                    $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
1328
                    $cropArea = $cropVariantCollection->getCropArea();
1329
                    $processedImage = $fileObject->process(
1330
                        ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
1331
                        [
1332
                            'width' => $sizeParts[0],
1333
                            'height' => $sizeParts[1] . 'c',
1334
                            'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject)
1335
                        ]
1336
                    );
1337
                    $imageUrl = $processedImage->getPublicUrl(true);
1338
                    $imgTag = '<img src="' . $imageUrl . '" '
1339
                        . 'width="' . $processedImage->getProperty('width') . '" '
1340
                        . 'height="' . $processedImage->getProperty('height') . '" '
1341
                        . 'alt="' . htmlspecialchars($fileReferenceObject->getName()) . '" />';
1342
                } else {
1343
                    // Icon
1344
                    $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1345
                        . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1346
                        . '</span>';
1347
                }
1348
                if ($linkInfoPopup) {
1349
                    $onClick = 'top.launchView(\'_FILE\',\'' . (int)$fileObject->getUid() . '\'); return false;';
1350
                    $thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $imgTag . '</a> ';
1351
                } else {
1352
                    $thumbData .= $imgTag;
1353
                }
1354
            }
1355
        } else {
1356
            // Find uploaddir automatically
1357
            if (is_null($uploaddir)) {
1358
                $uploaddir = $GLOBALS['TCA'][$table]['columns'][$field]['config']['uploadfolder'];
1359
            }
1360
            $uploaddir = rtrim($uploaddir, '/');
1361
            // Traverse files:
1362
            $thumbs = GeneralUtility::trimExplode(',', $row[$field], true);
1363
            $thumbData = '';
1364
            foreach ($thumbs as $theFile) {
1365
                if ($theFile) {
1366
                    $fileName = trim($uploaddir . '/' . $theFile, '/');
1367
                    try {
1368
                        /** @var File $fileObject */
1369
                        $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($fileName);
1370
                        // Skip the resource if it's not of type AbstractFile. One case where this can happen if the
1371
                        // storage has been externally modified and the field value now points to a folder
1372
                        // instead of a file.
1373
                        if (!$fileObject instanceof AbstractFile) {
1374
                            continue;
1375
                        }
1376
                        if ($fileObject->isMissing()) {
1377
                            $thumbData .= '<span class="label label-danger">'
1378
                                . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
1379
                                . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1380
                            continue;
1381
                        }
1382
                    } catch (ResourceDoesNotExistException $exception) {
1383
                        $thumbData .= '<span class="label label-danger">'
1384
                            . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
1385
                            . '</span>&nbsp;' . htmlspecialchars($fileName) . '<br />';
1386
                        continue;
1387
                    }
1388
1389
                    $fileExtension = $fileObject->getExtension();
1390
                    if ($fileExtension === 'ttf'
1391
                        || GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileExtension)
1392
                    ) {
1393
                        $imageUrl = $fileObject->process(
1394
                            ProcessedFile::CONTEXT_IMAGEPREVIEW,
1395
                            [
1396
                                'width' => $sizeParts[0],
1397
                                'height' => $sizeParts[1]
1398
                            ]
1399
                        )->getPublicUrl(true);
1400
1401
                        $image = '<img src="' . htmlspecialchars($imageUrl) . '" hspace="2" border="0" title="' . htmlspecialchars($fileObject->getName()) . '"' . $tparams . ' alt="" />';
1402
                        if ($linkInfoPopup) {
1403
                            $onClick = 'top.launchView(\'_FILE\', ' . GeneralUtility::quoteJSvalue($fileName) . ',\'\');return false;';
1404
                            $thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $image . '</a> ';
1405
                        } else {
1406
                            $thumbData .= $image;
1407
                        }
1408
                    } else {
1409
                        // Gets the icon
1410
                        $fileIcon = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1411
                            . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1412
                            . '</span>';
1413
                        if ($linkInfoPopup) {
1414
                            $onClick = 'top.launchView(\'_FILE\', ' . GeneralUtility::quoteJSvalue($fileName) . ',\'\'); return false;';
1415
                            $thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $fileIcon . '</a> ';
1416
                        } else {
1417
                            $thumbData .= $fileIcon;
1418
                        }
1419
                    }
1420
                }
1421
            }
1422
        }
1423
        return $thumbData;
1424
    }
1425
1426
    /**
1427
     * Returns title-attribute information for a page-record informing about id, alias, doktype, hidden, starttime, endtime, fe_group etc.
1428
     *
1429
     * @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)
1430
     * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4)
1431
     * @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
1432
     * @return string
1433
     */
1434
    public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true)
1435
    {
1436
        $lang = static::getLanguageService();
1437
        $parts = [];
1438
        $parts[] = 'id=' . $row['uid'];
1439
        if ($row['alias']) {
1440
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['alias']['label']) . ' ' . $row['alias'];
1441
        }
1442
        if ($row['pid'] < 0) {
1443
            $parts[] = 'v#1.' . $row['t3ver_id'];
1444
        }
1445
        switch (VersionState::cast($row['t3ver_state'])) {
1446
            case new VersionState(VersionState::NEW_PLACEHOLDER):
1447
                $parts[] = 'PLH WSID#' . $row['t3ver_wsid'];
1448
                break;
1449
            case new VersionState(VersionState::DELETE_PLACEHOLDER):
1450
                $parts[] = 'Deleted element!';
1451
                break;
1452
            case new VersionState(VersionState::MOVE_PLACEHOLDER):
1453
                $parts[] = 'NEW LOCATION (PLH) WSID#' . $row['t3ver_wsid'];
1454
                break;
1455
            case new VersionState(VersionState::MOVE_POINTER):
1456
                $parts[] = 'OLD LOCATION (PNT) WSID#' . $row['t3ver_wsid'];
1457
                break;
1458
            case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1459
                $parts[] = 'New element!';
1460
                break;
1461
        }
1462
        if ($row['doktype'] == PageRepository::DOKTYPE_LINK) {
1463
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label']) . ' ' . $row['url'];
1464
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1465
            if ($perms_clause) {
1466
                $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20);
1467
            } else {
1468
                $row['shortcut'] = (int)$row['shortcut'];
1469
                $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title');
1470
                $label = $lRec['title'] . ' (id=' . $row['shortcut'] . ')';
1471
            }
1472
            if ($row['shortcut_mode'] != PageRepository::SHORTCUT_MODE_NONE) {
1473
                $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' '
1474
                    . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode']));
1475
            }
1476
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . $label;
0 ignored issues
show
Bug introduced by
Are you sure $label of type string|array<integer,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

1476
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . /** @scrutinizer ignore-type */ $label;
Loading history...
1477
        } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) {
1478
            if ($perms_clause) {
1479
                $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20);
1480
            } else {
1481
                $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title');
1482
                $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')';
1483
            }
1484
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label;
1485
            if ($row['mount_pid_ol']) {
1486
                $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']);
1487
            }
1488
        }
1489
        if ($row['nav_hide']) {
1490
            $parts[] = rtrim($lang->sL($GLOBALS['TCA']['pages']['columns']['nav_hide']['label']), ':');
1491
        }
1492
        if ($row['hidden']) {
1493
            $parts[] = $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.hidden');
1494
        }
1495
        if ($row['starttime']) {
1496
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label'])
1497
                . ' ' . self::dateTimeAge($row['starttime'], -1, 'date');
1498
        }
1499
        if ($row['endtime']) {
1500
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' '
1501
                . self::dateTimeAge($row['endtime'], -1, 'date');
1502
        }
1503
        if ($row['fe_group']) {
1504
            $fe_groups = [];
1505
            foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) {
1506
                if ($fe_group < 0) {
1507
                    $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', $fe_group));
1508
                } else {
1509
                    $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title');
1510
                    $fe_groups[] = $lRec['title'];
1511
                }
1512
            }
1513
            $label = implode(', ', $fe_groups);
1514
            $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label;
1515
        }
1516
        $out = htmlspecialchars(implode(' - ', $parts));
1517
        return $includeAttrib ? 'title="' . $out . '"' : $out;
1518
    }
1519
1520
    /**
1521
     * Returns the combined markup for Bootstraps tooltips
1522
     *
1523
     * @param array $row
1524
     * @param string $table
1525
     * @return string
1526
     */
1527
    public static function getRecordToolTip(array $row, $table = 'pages')
1528
    {
1529
        $toolTipText = self::getRecordIconAltText($row, $table);
1530
        $toolTipCode = 'data-toggle="tooltip" data-title=" '
1531
            . str_replace(' - ', '<br>', $toolTipText)
1532
            . '" data-html="true" data-placement="right"';
1533
        return $toolTipCode;
1534
    }
1535
1536
    /**
1537
     * Returns title-attribute information for ANY record (from a table defined in TCA of course)
1538
     * 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.
1539
     * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page.
1540
     *
1541
     * @param array $row Table row; $row is a row from the table, $table
1542
     * @param string $table Table name
1543
     * @return string
1544
     */
1545
    public static function getRecordIconAltText($row, $table = 'pages')
1546
    {
1547
        if ($table === 'pages') {
1548
            $out = self::titleAttribForPages($row, '', 0);
1549
        } else {
1550
            $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'])) ? $row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] . ' ' : '';
1551
            $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
1552
            // Uid is added
1553
            $out .= 'id=' . $row['uid'];
1554
            if ($table === 'pages' && $row['alias']) {
1555
                $out .= ' / ' . $row['alias'];
1556
            }
1557
            if (static::isTableWorkspaceEnabled($table) && $row['pid'] < 0) {
1558
                $out .= ' - v#1.' . $row['t3ver_id'];
1559
            }
1560
            if (static::isTableWorkspaceEnabled($table)) {
1561
                switch (VersionState::cast($row['t3ver_state'])) {
1562
                    case new VersionState(VersionState::NEW_PLACEHOLDER):
1563
                        $out .= ' - PLH WSID#' . $row['t3ver_wsid'];
1564
                        break;
1565
                    case new VersionState(VersionState::DELETE_PLACEHOLDER):
1566
                        $out .= ' - Deleted element!';
1567
                        break;
1568
                    case new VersionState(VersionState::MOVE_PLACEHOLDER):
1569
                        $out .= ' - NEW LOCATION (PLH) WSID#' . $row['t3ver_wsid'];
1570
                        break;
1571
                    case new VersionState(VersionState::MOVE_POINTER):
1572
                        $out .= ' - OLD LOCATION (PNT)  WSID#' . $row['t3ver_wsid'];
1573
                        break;
1574
                    case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1575
                        $out .= ' - New element!';
1576
                        break;
1577
                }
1578
            }
1579
            // Hidden
1580
            $lang = static::getLanguageService();
1581
            if ($ctrl['disabled']) {
1582
                $out .= $row[$ctrl['disabled']] ? ' - ' . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.hidden') : '';
1583
            }
1584
            if ($ctrl['starttime']) {
1585
                if ($row[$ctrl['starttime']] > $GLOBALS['EXEC_TIME']) {
1586
                    $out .= ' - ' . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.starttime') . ':' . self::date($row[$ctrl['starttime']]) . ' (' . self::daysUntil($row[$ctrl['starttime']]) . ' ' . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.days') . ')';
1587
                }
1588
            }
1589
            if ($row[$ctrl['endtime']]) {
1590
                $out .= ' - ' . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.endtime') . ': ' . self::date($row[$ctrl['endtime']]) . ' (' . self::daysUntil($row[$ctrl['endtime']]) . ' ' . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.days') . ')';
1591
            }
1592
        }
1593
        return htmlspecialchars($out);
1594
    }
1595
1596
    /**
1597
     * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key
1598
     *
1599
     * @param string $table Table name, present in $GLOBALS['TCA']
1600
     * @param string $col Field name, present in $GLOBALS['TCA']
1601
     * @param string $key items-array value to match
1602
     * @return string Label for item entry
1603
     */
1604
    public static function getLabelFromItemlist($table, $col, $key)
1605
    {
1606
        // Check, if there is an "items" array:
1607
        if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) {
1608
            // Traverse the items-array...
1609
            foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) {
1610
                // ... and return the first found label where the value was equal to $key
1611
                if ((string)$v[1] === (string)$key) {
1612
                    return $v[0];
1613
                }
1614
            }
1615
        }
1616
        return '';
1617
    }
1618
1619
    /**
1620
     * Return the label of a field by additionally checking TsConfig values
1621
     *
1622
     * @param int $pageId Page id
1623
     * @param string $table Table name
1624
     * @param string $column Field Name
1625
     * @param string $key item value
1626
     * @return string Label for item entry
1627
     */
1628
    public static function getLabelFromItemListMerged($pageId, $table, $column, $key)
1629
    {
1630
        $pageTsConfig = static::getPagesTSconfig($pageId);
1631
        $label = '';
1632
        if (is_array($pageTsConfig['TCEFORM.'])
1633
            && is_array($pageTsConfig['TCEFORM.'][$table . '.'])
1634
            && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.'])
1635
        ) {
1636
            if (is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'])
1637
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key])
1638
            ) {
1639
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key];
1640
            } elseif (is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'])
1641
                && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key])
1642
            ) {
1643
                $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key];
1644
            }
1645
        }
1646
        if (empty($label)) {
1647
            $tcaValue = self::getLabelFromItemlist($table, $column, $key);
1648
            if (!empty($tcaValue)) {
1649
                $label = $tcaValue;
1650
            }
1651
        }
1652
        return $label;
1653
    }
1654
1655
    /**
1656
     * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma.
1657
     * NOTE: this does not take itemsProcFunc into account
1658
     *
1659
     * @param string $table Table name, present in TCA
1660
     * @param string $column Field name
1661
     * @param string $keyList Key or comma-separated list of keys.
1662
     * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>)
1663
     * @return string Comma-separated list of localized labels
1664
     */
1665
    public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = [])
1666
    {
1667
        // Check if there is an "items" array
1668
        if (
1669
            !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1670
            || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1671
            || $keyList === ''
1672
        ) {
1673
            return '';
1674
        }
1675
1676
        $keys = GeneralUtility::trimExplode(',', $keyList, true);
1677
        $labels = [];
1678
        // Loop on all selected values
1679
        foreach ($keys as $key) {
1680
            $label = null;
1681
            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...
1682
                // Check if label has been defined or redefined via pageTsConfig
1683
                if (isset($columnTsConfig['addItems.'][$key])) {
1684
                    $label = $columnTsConfig['addItems.'][$key];
1685
                } elseif (isset($columnTsConfig['altLabels.'][$key])) {
1686
                    $label = $columnTsConfig['altLabels.'][$key];
1687
                }
1688
            }
1689
            if ($label === null) {
1690
                // Otherwise lookup the label in TCA items list
1691
                foreach ($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'] as $itemConfiguration) {
1692
                    list($currentLabel, $currentKey) = $itemConfiguration;
1693
                    if ((string)$key === (string)$currentKey) {
1694
                        $label = $currentLabel;
1695
                        break;
1696
                    }
1697
                }
1698
            }
1699
            if ($label !== null) {
1700
                $labels[] = static::getLanguageService()->sL($label);
1701
            }
1702
        }
1703
        return implode(', ', $labels);
1704
    }
1705
1706
    /**
1707
     * Returns the label-value for fieldname $col in table, $table
1708
     * 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>'
1709
     *
1710
     * @param string $table Table name, present in $GLOBALS['TCA']
1711
     * @param string $col Field name
1712
     * @return string or NULL if $col is not found in the TCA table
1713
     */
1714
    public static function getItemLabel($table, $col)
1715
    {
1716
        // Check if column exists
1717
        if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1718
            return $GLOBALS['TCA'][$table]['columns'][$col]['label'];
1719
        }
1720
1721
        return null;
1722
    }
1723
1724
    /**
1725
     * Returns the "title"-value in record, $row, from table, $table
1726
     * The field(s) from which the value is taken is determined by the "ctrl"-entries 'label', 'label_alt' and 'label_alt_force'
1727
     *
1728
     * @param string $table Table name, present in TCA
1729
     * @param array $row Row from table
1730
     * @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
1731
     * @param bool $forceResult If set, the function always returns an output. If no value is found for the title, '[No title]' is returned (localized).
1732
     * @return string
1733
     */
1734
    public static function getRecordTitle($table, $row, $prep = false, $forceResult = true)
1735
    {
1736
        $recordTitle = '';
1737
        if (is_array($GLOBALS['TCA'][$table])) {
1738
            // If configured, call userFunc
1739
            if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'])) {
1740
                $params['table'] = $table;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
1741
                $params['row'] = $row;
1742
                $params['title'] = '';
1743
                $params['options'] = $GLOBALS['TCA'][$table]['ctrl']['label_userFunc_options'] ?? [];
1744
1745
                // Create NULL-reference
1746
                $null = null;
1747
                GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'], $params, $null);
1748
                $recordTitle = $params['title'];
1749
            } else {
1750
                // No userFunc: Build label
1751
                $recordTitle = self::getProcessedValue(
1752
                    $table,
1753
                    $GLOBALS['TCA'][$table]['ctrl']['label'],
1754
                    $row[$GLOBALS['TCA'][$table]['ctrl']['label']],
1755
                    0,
1756
                    0,
1757
                    false,
1758
                    $row['uid'],
1759
                    $forceResult
1760
                );
1761
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])
1762
                    && (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) || (string)$recordTitle === '')
1763
                ) {
1764
                    $altFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
1765
                    $tA = [];
1766
                    if (!empty($recordTitle)) {
1767
                        $tA[] = $recordTitle;
1768
                    }
1769
                    foreach ($altFields as $fN) {
1770
                        $recordTitle = trim(strip_tags($row[$fN]));
1771
                        if ((string)$recordTitle !== '') {
1772
                            $recordTitle = self::getProcessedValue($table, $fN, $recordTitle, 0, 0, false, $row['uid']);
1773
                            if (!$GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1774
                                break;
1775
                            }
1776
                            $tA[] = $recordTitle;
1777
                        }
1778
                    }
1779
                    if ($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1780
                        $recordTitle = implode(', ', $tA);
1781
                    }
1782
                }
1783
            }
1784
            // If the current result is empty, set it to '[No title]' (localized) and prepare for output if requested
1785
            if ($prep || $forceResult) {
1786
                if ($prep) {
1787
                    $recordTitle = self::getRecordTitlePrep($recordTitle);
1788
                }
1789
                if (trim($recordTitle) === '') {
1790
                    $recordTitle = self::getNoRecordTitle($prep);
1791
                }
1792
            }
1793
        }
1794
1795
        return $recordTitle;
1796
    }
1797
1798
    /**
1799
     * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>,
1800
     * which offers a tooltip with the original title when moving mouse over it.
1801
     *
1802
     * @param string $title The title string to be cropped
1803
     * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
1804
     * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
1805
     */
1806
    public static function getRecordTitlePrep($title, $titleLength = 0)
1807
    {
1808
        // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
1809
        if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
1810
            $titleLength = static::getBackendUserAuthentication()->uc['titleLen'];
1811
        }
1812
        $titleOrig = htmlspecialchars($title);
1813
        $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
1814
        // If title was cropped, offer a tooltip:
1815
        if ($titleOrig != $title) {
1816
            $title = '<span title="' . $titleOrig . '">' . $title . '</span>';
1817
        }
1818
        return $title;
1819
    }
1820
1821
    /**
1822
     * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE.
1823
     *
1824
     * @param bool $prep Wrap result in <em>|</em>
1825
     * @return string Localized [No title] string
1826
     */
1827
    public static function getNoRecordTitle($prep = false)
1828
    {
1829
        $noTitle = '[' .
1830
            htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title'))
1831
            . ']';
1832
        if ($prep) {
1833
            $noTitle = '<em>' . $noTitle . '</em>';
1834
        }
1835
        return $noTitle;
1836
    }
1837
1838
    /**
1839
     * Returns a human readable output of a value from a record
1840
     * 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.
1841
     * $table/$col is tablename and fieldname
1842
     * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant)
1843
     *
1844
     * @param string $table Table name, present in TCA
1845
     * @param string $col Field name, present in TCA
1846
     * @param string $value The value of that field from a selected record
1847
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1848
     * @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")
1849
     * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown.
1850
     * @param int $uid Uid of the current record
1851
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1852
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1853
     * @throws \InvalidArgumentException
1854
     * @return string|null
1855
     */
1856
    public static function getProcessedValue(
1857
        $table,
1858
        $col,
1859
        $value,
1860
        $fixed_lgd_chars = 0,
1861
        $defaultPassthrough = false,
1862
        $noRecordLookup = false,
1863
        $uid = 0,
1864
        $forceResult = true,
1865
        $pid = 0
1866
    ) {
1867
        if ($col === 'uid') {
1868
            // uid is not in TCA-array
1869
            return $value;
1870
        }
1871
        // Check if table and field is configured
1872
        if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1873
            return null;
1874
        }
1875
        // Depending on the fields configuration, make a meaningful output value.
1876
        $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
1877
        /*****************
1878
         *HOOK: pre-processing the human readable output from a record
1879
         ****************/
1880
        $null = null;
1881
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) {
1882
            GeneralUtility::callUserFunction($_funcRef, $theColConf, $null);
1883
        }
1884
1885
        $l = '';
1886
        $lang = static::getLanguageService();
1887
        switch ((string)$theColConf['type']) {
1888
            case 'radio':
1889
                $l = self::getLabelFromItemlist($table, $col, $value);
1890
                $l = $lang->sL($l);
1891
                break;
1892
            case 'inline':
1893
            case 'select':
1894
                if (!empty($theColConf['MM'])) {
1895
                    if ($uid) {
1896
                        // Display the title of MM related records in lists
1897
                        if ($noRecordLookup) {
1898
                            $MMfields = [];
1899
                            $MMfields[] = $theColConf['foreign_table'] . '.uid';
1900
                        } else {
1901
                            $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1902
                            foreach (GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'], true) as $f) {
1903
                                $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1904
                            }
1905
                        }
1906
                        /** @var $dbGroup RelationHandler */
1907
                        $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1908
                        $dbGroup->start(
1909
                            $value,
1910
                            $theColConf['foreign_table'],
1911
                            $theColConf['MM'],
1912
                            $uid,
1913
                            $table,
1914
                            $theColConf
1915
                        );
1916
                        $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1917
                        if (is_array($selectUids) && !empty($selectUids)) {
1918
                            $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1919
                            $queryBuilder->getRestrictions()
1920
                                ->removeAll()
1921
                                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1922
1923
                            $result = $queryBuilder
1924
                                ->select('uid', ...$MMfields)
1925
                                ->from($theColConf['foreign_table'])
1926
                                ->where(
1927
                                    $queryBuilder->expr()->in(
1928
                                        'uid',
1929
                                        $queryBuilder->createNamedParameter($selectUids, Connection::PARAM_INT_ARRAY)
1930
                                    )
1931
                                )
1932
                                ->execute();
1933
1934
                            $mmlA = [];
1935
                            while ($MMrow = $result->fetch()) {
1936
                                // Keep sorting of $selectUids
1937
                                $selectedUid = array_search($MMrow['uid'], $selectUids);
1938
                                $mmlA[$selectedUid] = $MMrow['uid'];
1939
                                if (!$noRecordLookup) {
1940
                                    $mmlA[$selectedUid] = static::getRecordTitle(
1941
                                        $theColConf['foreign_table'],
1942
                                        $MMrow,
1943
                                        false,
1944
                                        $forceResult
1945
                                    );
1946
                                }
1947
                            }
1948
1949
                            if (!empty($mmlA)) {
1950
                                ksort($mmlA);
1951
                                $l = implode('; ', $mmlA);
1952
                            } else {
1953
                                $l = 'N/A';
1954
                            }
1955
                        } else {
1956
                            $l = 'N/A';
1957
                        }
1958
                    } else {
1959
                        $l = 'N/A';
1960
                    }
1961
                } else {
1962
                    $columnTsConfig = [];
1963
                    if ($pid) {
1964
                        $pageTsConfig = self::getPagesTSconfig($pid);
1965
                        if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) {
1966
                            $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'];
1967
                        }
1968
                    }
1969
                    $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig);
1970
                    if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) {
1971
                        if ($noRecordLookup) {
1972
                            $l = $value;
1973
                        } else {
1974
                            $rParts = [];
1975
                            if ($uid && isset($theColConf['foreign_field']) && $theColConf['foreign_field'] !== '') {
1976
                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1977
                                $queryBuilder->getRestrictions()
1978
                                    ->removeAll()
1979
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1980
                                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1981
                                $constraints = [
1982
                                    $queryBuilder->expr()->eq(
1983
                                        $theColConf['foreign_field'],
1984
                                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1985
                                    )
1986
                                ];
1987
1988
                                if (!empty($theColConf['foreign_table_field'])) {
1989
                                    $constraints[] = $queryBuilder->expr()->eq(
1990
                                        $theColConf['foreign_table_field'],
1991
                                        $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
1992
                                    );
1993
                                }
1994
1995
                                // Add additional where clause if foreign_match_fields are defined
1996
                                $foreignMatchFields = [];
1997
                                if (is_array($theColConf['foreign_match_fields'])) {
1998
                                    $foreignMatchFields =  $theColConf['foreign_match_fields'];
1999
                                }
2000
2001
                                foreach ($foreignMatchFields as $matchField => $matchValue) {
2002
                                    $constraints[] = $queryBuilder->expr()->eq(
2003
                                        $matchField,
2004
                                        $queryBuilder->createNamedParameter($matchValue)
2005
                                    );
2006
                                }
2007
2008
                                $result = $queryBuilder
2009
                                    ->select('*')
2010
                                    ->from($theColConf['foreign_table'])
2011
                                    ->where(...$constraints)
2012
                                    ->execute();
2013
2014
                                while ($record = $result->fetch()) {
2015
                                    $rParts[] = $record['uid'];
2016
                                }
2017
                            }
2018
                            if (empty($rParts)) {
2019
                                $rParts = GeneralUtility::trimExplode(',', $value, true);
2020
                            }
2021
                            $lA = [];
2022
                            foreach ($rParts as $rVal) {
2023
                                $rVal = (int)$rVal;
2024
                                $r = self::getRecordWSOL($theColConf['foreign_table'], $rVal);
2025
                                if (is_array($r)) {
2026
                                    $lA[] = $lang->sL($theColConf['foreign_table_prefix'])
2027
                                        . self::getRecordTitle($theColConf['foreign_table'], $r, false, $forceResult);
2028
                                } else {
2029
                                    $lA[] = $rVal ? '[' . $rVal . '!]' : '';
2030
                                }
2031
                            }
2032
                            $l = implode(', ', $lA);
2033
                        }
2034
                    }
2035
                    if (empty($l) && !empty($value)) {
2036
                        // Use plain database value when label is empty
2037
                        $l = $value;
2038
                    }
2039
                }
2040
                break;
2041
            case 'group':
2042
                // resolve the titles for DB records
2043
                if ($theColConf['internal_type'] === 'db') {
2044
                    if ($theColConf['MM']) {
2045
                        if ($uid) {
2046
                            // Display the title of MM related records in lists
2047
                            if ($noRecordLookup) {
2048
                                $MMfields = [];
2049
                                $MMfields[] = $theColConf['foreign_table'] . '.uid';
2050
                            } else {
2051
                                $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
2052
                                $altLabelFields = explode(
2053
                                    ',',
2054
                                    $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt']
2055
                                );
2056
                                foreach ($altLabelFields as $f) {
2057
                                    $f = trim($f);
2058
                                    if ($f !== '') {
2059
                                        $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
2060
                                    }
2061
                                }
2062
                            }
2063
                            /** @var $dbGroup RelationHandler */
2064
                            $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
2065
                            $dbGroup->start(
2066
                                $value,
2067
                                $theColConf['foreign_table'],
2068
                                $theColConf['MM'],
2069
                                $uid,
2070
                                $table,
2071
                                $theColConf
2072
                            );
2073
                            $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
2074
                            if (!empty($selectUids) && is_array($selectUids)) {
2075
                                $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
2076
                                $queryBuilder->getRestrictions()
2077
                                    ->removeAll()
2078
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
2079
2080
                                $result = $queryBuilder
2081
                                    ->select('uid', ...$MMfields)
2082
                                    ->from($theColConf['foreign_table'])
2083
                                    ->where(
2084
                                        $queryBuilder->expr()->in(
2085
                                            'uid',
2086
                                            $queryBuilder->createNamedParameter(
2087
                                                $selectUids,
2088
                                                Connection::PARAM_INT_ARRAY
2089
                                            )
2090
                                        )
2091
                                    )
2092
                                    ->execute();
2093
2094
                                $mmlA = [];
2095
                                while ($MMrow = $result->fetch()) {
2096
                                    // Keep sorting of $selectUids
2097
                                    $selectedUid = array_search($MMrow['uid'], $selectUids);
2098
                                    $mmlA[$selectedUid] =  $MMrow['uid'];
2099
                                    if (!$noRecordLookup) {
2100
                                        $mmlA[$selectedUid] =  static::getRecordTitle(
2101
                                            $theColConf['foreign_table'],
2102
                                            $MMrow,
2103
                                            false,
2104
                                            $forceResult
2105
                                        );
2106
                                    }
2107
                                }
2108
2109
                                if (!empty($mmlA)) {
2110
                                    ksort($mmlA);
2111
                                    $l = implode('; ', $mmlA);
2112
                                } else {
2113
                                    $l = 'N/A';
2114
                                }
2115
                            } else {
2116
                                $l = 'N/A';
2117
                            }
2118
                        } else {
2119
                            $l = 'N/A';
2120
                        }
2121
                    } else {
2122
                        $finalValues = [];
2123
                        $relationTableName = $theColConf['allowed'];
2124
                        $explodedValues = GeneralUtility::trimExplode(',', $value, true);
2125
2126
                        foreach ($explodedValues as $explodedValue) {
2127
                            if (MathUtility::canBeInterpretedAsInteger($explodedValue)) {
2128
                                $relationTableNameForField = $relationTableName;
2129
                            } else {
2130
                                list($relationTableNameForField, $explodedValue) = self::splitTable_Uid($explodedValue);
2131
                            }
2132
2133
                            $relationRecord = static::getRecordWSOL($relationTableNameForField, $explodedValue);
2134
                            $finalValues[] = static::getRecordTitle($relationTableNameForField, $relationRecord);
2135
                        }
2136
                        $l = implode(', ', $finalValues);
2137
                    }
2138
                } else {
2139
                    $l = implode(', ', GeneralUtility::trimExplode(',', $value, true));
2140
                }
2141
                break;
2142
            case 'check':
2143
                if (!is_array($theColConf['items']) || count($theColConf['items']) === 1) {
2144
                    $l = $value ? $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:yes') : $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:no');
2145
                } else {
2146
                    $lA = [];
2147
                    foreach ($theColConf['items'] as $key => $val) {
2148
                        if ($value & pow(2, $key)) {
2149
                            $lA[] = $lang->sL($val[0]);
2150
                        }
2151
                    }
2152
                    $l = implode(', ', $lA);
2153
                }
2154
                break;
2155
            case 'input':
2156
                // Hide value 0 for dates, but show it for everything else
2157
                if (isset($value)) {
2158
                    $dateTimeFormats = QueryHelper::getDateTimeFormats();
2159
2160
                    if (GeneralUtility::inList($theColConf['eval'], 'date')) {
2161
                        // Handle native date field
2162
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') {
2163
                            $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value);
2164
                        } else {
2165
                            $value = (int)$value;
2166
                        }
2167
                        if (!empty($value)) {
2168
                            $ageSuffix = '';
2169
                            $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
2170
                            $ageDisplayKey = 'disableAgeDisplay';
2171
2172
                            // generate age suffix as long as not explicitly suppressed
2173
                            if (!isset($dateColumnConfiguration[$ageDisplayKey])
2174
                                // non typesafe comparison on intention
2175
                                || $dateColumnConfiguration[$ageDisplayKey] == false
2176
                            ) {
2177
                                $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '')
2178
                                    . self::calcAge(
2179
                                        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

2179
                                        /** @scrutinizer ignore-type */ abs(($GLOBALS['EXEC_TIME'] - $value)),
Loading history...
2180
                                        $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2181
                                    )
2182
                                    . ')';
2183
                            }
2184
2185
                            $l = self::date($value) . $ageSuffix;
2186
                        }
2187
                    } elseif (GeneralUtility::inList($theColConf['eval'], 'time')) {
2188
                        // Handle native time field
2189
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2190
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
2191
                        } else {
2192
                            $value = (int)$value;
2193
                        }
2194
                        if (!empty($value)) {
2195
                            $l = gmdate('H:i', (int)$value);
2196
                        }
2197
                    } elseif (GeneralUtility::inList($theColConf['eval'], 'timesec')) {
2198
                        // Handle native time field
2199
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2200
                            $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
2201
                        } else {
2202
                            $value = (int)$value;
2203
                        }
2204
                        if (!empty($value)) {
2205
                            $l = gmdate('H:i:s', (int)$value);
2206
                        }
2207
                    } elseif (GeneralUtility::inList($theColConf['eval'], 'datetime')) {
2208
                        // Handle native datetime field
2209
                        if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') {
2210
                            $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value);
2211
                        } else {
2212
                            $value = (int)$value;
2213
                        }
2214
                        if (!empty($value)) {
2215
                            $l = self::datetime($value);
2216
                        }
2217
                    } else {
2218
                        $l = $value;
2219
                    }
2220
                }
2221
                break;
2222
            case 'flex':
2223
                $l = strip_tags($value);
2224
                break;
2225
            default:
2226
                if ($defaultPassthrough) {
2227
                    $l = $value;
2228
                } elseif ($theColConf['MM']) {
2229
                    $l = 'N/A';
2230
                } elseif ($value) {
2231
                    $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200);
2232
                }
2233
        }
2234
        // If this field is a password field, then hide the password by changing it to a random number of asterisk (*)
2235
        if (!empty($theColConf['eval']) && stristr($theColConf['eval'], 'password')) {
2236
            $l = '';
2237
            $randomNumber = rand(5, 12);
2238
            for ($i = 0; $i < $randomNumber; $i++) {
2239
                $l .= '*';
2240
            }
2241
        }
2242
        /*****************
2243
         *HOOK: post-processing the human readable output from a record
2244
         ****************/
2245
        $null = null;
2246
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) {
2247
            $params = [
2248
                'value' => $l,
2249
                'colConf' => $theColConf
2250
            ];
2251
            $l = GeneralUtility::callUserFunction($_funcRef, $params, $null);
2252
        }
2253
        if ($fixed_lgd_chars) {
2254
            return GeneralUtility::fixed_lgd_cs($l, $fixed_lgd_chars);
2255
        }
2256
        return $l;
2257
    }
2258
2259
    /**
2260
     * 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.
2261
     *
2262
     * @param string $table Table name, present in TCA
2263
     * @param string $fN Field name
2264
     * @param string $fV Field value
2265
     * @param int $fixed_lgd_chars The max amount of characters the value may occupy
2266
     * @param int $uid Uid of the current record
2267
     * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
2268
     * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
2269
     * @return string
2270
     * @see getProcessedValue()
2271
     */
2272
    public static function getProcessedValueExtra(
2273
        $table,
2274
        $fN,
2275
        $fV,
2276
        $fixed_lgd_chars = 0,
2277
        $uid = 0,
2278
        $forceResult = true,
2279
        $pid = 0
2280
    ) {
2281
        $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, 1, 0, $uid, $forceResult, $pid);
2282
        if (!isset($fVnew)) {
2283
            if (is_array($GLOBALS['TCA'][$table])) {
2284
                if ($fN == $GLOBALS['TCA'][$table]['ctrl']['tstamp'] || $fN == $GLOBALS['TCA'][$table]['ctrl']['crdate']) {
2285
                    $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

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

2288
                    $fVnew = self::getRecordPath(/** @scrutinizer ignore-type */ $fV, '1=1', 20);
Loading history...
2289
                } else {
2290
                    $fVnew = $fV;
2291
                }
2292
            }
2293
        }
2294
        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...
2295
    }
2296
2297
    /**
2298
     * Returns fields for a table, $table, which would typically be interesting to select
2299
     * This includes uid, the fields defined for title, icon-field.
2300
     * 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)
2301
     *
2302
     * @param string $table Table name, present in $GLOBALS['TCA']
2303
     * @param string $prefix Table prefix
2304
     * @param array $fields Preset fields (must include prefix if that is used)
2305
     * @return string List of fields.
2306
     */
2307
    public static function getCommonSelectFields($table, $prefix = '', $fields = [])
2308
    {
2309
        $fields[] = $prefix . 'uid';
2310
        if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
2311
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
2312
        }
2313
        if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
2314
            $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
2315
            foreach ($secondFields as $fieldN) {
2316
                $fields[] = $prefix . $fieldN;
2317
            }
2318
        }
2319
        if (static::isTableWorkspaceEnabled($table)) {
2320
            $fields[] = $prefix . 't3ver_id';
2321
            $fields[] = $prefix . 't3ver_state';
2322
            $fields[] = $prefix . 't3ver_wsid';
2323
            $fields[] = $prefix . 't3ver_count';
2324
        }
2325
        if ($GLOBALS['TCA'][$table]['ctrl']['selicon_field']) {
2326
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
2327
        }
2328
        if ($GLOBALS['TCA'][$table]['ctrl']['typeicon_column']) {
2329
            $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
2330
        }
2331
        if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
2332
            if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']) {
2333
                $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2334
            }
2335
            if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime']) {
2336
                $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
2337
            }
2338
            if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime']) {
2339
                $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
2340
            }
2341
            if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group']) {
2342
                $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
2343
            }
2344
        }
2345
        return implode(',', array_unique($fields));
2346
    }
2347
2348
    /*******************************************
2349
     *
2350
     * Backend Modules API functions
2351
     *
2352
     *******************************************/
2353
2354
    /**
2355
     * Returns CSH help text (description), if configured for, as an array (title, description)
2356
     *
2357
     * @param string $table Table name
2358
     * @param string $field Field name
2359
     * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
2360
     */
2361
    public static function helpTextArray($table, $field)
2362
    {
2363
        if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
2364
            static::getLanguageService()->loadSingleTableDescription($table);
2365
        }
2366
        $output = [
2367
            'description' => null,
2368
            'title' => null,
2369
            'moreInfo' => false
2370
        ];
2371
        if (is_array($GLOBALS['TCA_DESCR'][$table]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
2372
            $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
2373
            // Add alternative title, if defined
2374
            if ($data['alttitle']) {
2375
                $output['title'] = $data['alttitle'];
2376
            }
2377
            // If we have more information to show and access to the cshmanual
2378
            if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax'])
2379
                && static::getBackendUserAuthentication()->check('modules', 'help_CshmanualCshmanual')
2380
            ) {
2381
                $output['moreInfo'] = true;
2382
            }
2383
            // Add description
2384
            if ($data['description']) {
2385
                $output['description'] = $data['description'];
2386
            }
2387
        }
2388
        return $output;
2389
    }
2390
2391
    /**
2392
     * Returns CSH help text
2393
     *
2394
     * @param string $table Table name
2395
     * @param string $field Field name
2396
     * @return string HTML content for help text
2397
     * @see cshItem()
2398
     */
2399
    public static function helpText($table, $field)
2400
    {
2401
        $helpTextArray = self::helpTextArray($table, $field);
2402
        $output = '';
2403
        $arrow = '';
2404
        // Put header before the rest of the text
2405
        if ($helpTextArray['title'] !== null) {
2406
            $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
2407
        }
2408
        // Add see also arrow if we have more info
2409
        if ($helpTextArray['moreInfo']) {
2410
            /** @var IconFactory $iconFactory */
2411
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2412
            $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
2413
        }
2414
        // Wrap description and arrow in p tag
2415
        if ($helpTextArray['description'] !== null || $arrow) {
2416
            $output .= '<p class="t3-help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>';
2417
        }
2418
        return $output;
2419
    }
2420
2421
    /**
2422
     * API function that wraps the text / html in help text, so if a user hovers over it
2423
     * the help text will show up
2424
     * This is the new help API function since TYPO3 4.5, and uses the new behaviour
2425
     * (hover over text, no icon, no fulltext option, no option to disable the help)
2426
     *
2427
     * @param string $table The table name for which the help should be shown
2428
     * @param string $field The field name for which the help should be shown
2429
     * @param string $text The text which should be wrapped with the help text
2430
     * @param array $overloadHelpText Array with text to overload help text
2431
     * @return string the HTML code ready to render
2432
     */
2433
    public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
2434
    {
2435
        // Initialize some variables
2436
        $helpText = '';
2437
        $abbrClassAdd = '';
2438
        $hasHelpTextOverload = !empty($overloadHelpText);
2439
        // Get the help text that should be shown on hover
2440
        if (!$hasHelpTextOverload) {
2441
            $helpText = self::helpText($table, $field);
2442
        }
2443
        // If there's a help text or some overload information, proceed with preparing an output
2444
        // @todo: right now this is a hard dependency on csh manual, as the whole help system should be moved to
2445
        // the extension. The core provides an API for adding help and rendering help, but the rendering
2446
        // should be up to the extension itself
2447
        if ((!empty($helpText) || $hasHelpTextOverload) && ExtensionManagementUtility::isLoaded('documentation')) {
2448
            // If no text was given, just use the regular help icon
2449
            if ($text == '') {
2450
                /** @var IconFactory $iconFactory */
2451
                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2452
                $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2453
                $abbrClassAdd = '-icon';
2454
            }
2455
            $text = '<abbr class="t3-help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2456
            $wrappedText = '<span class="t3-help-link" href="#" data-table="' . $table . '" data-field="' . $field . '"';
2457
            // The overload array may provide a title and a description
2458
            // If either one is defined, add them to the "data" attributes
2459
            if ($hasHelpTextOverload) {
2460
                if (isset($overloadHelpText['title'])) {
2461
                    $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2462
                }
2463
                if (isset($overloadHelpText['description'])) {
2464
                    $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2465
                }
2466
            }
2467
            $wrappedText .= '>' . $text . '</span>';
2468
            return $wrappedText;
2469
        }
2470
        return $text;
2471
    }
2472
2473
    /**
2474
     * API for getting CSH icons/text for use in backend modules.
2475
     * TCA_DESCR will be loaded if it isn't already
2476
     *
2477
     * @param string $table Table name ('_MOD_'+module name)
2478
     * @param string $field Field name (CSH locallang main key)
2479
     * @param string $_ (unused)
2480
     * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2481
     * @return string HTML content for help text
2482
     */
2483
    public static function cshItem($table, $field, $_ = '', $wrap = '')
0 ignored issues
show
Unused Code introduced by
The parameter $_ is not used and could be removed. ( Ignorable by Annotation )

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

2483
    public static function cshItem($table, $field, /** @scrutinizer ignore-unused */ $_ = '', $wrap = '')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2484
    {
2485
        static::getLanguageService()->loadSingleTableDescription($table);
2486
        if (is_array($GLOBALS['TCA_DESCR'][$table])
2487
            && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])
2488
        ) {
2489
            // Creating short description
2490
            $output = self::wrapInHelp($table, $field);
2491
            if ($output && $wrap) {
2492
                $wrParts = explode('|', $wrap);
2493
                $output = $wrParts[0] . $output . $wrParts[1];
2494
            }
2495
            return $output;
2496
        }
2497
        return '';
2498
    }
2499
2500
    /**
2501
     * Returns a JavaScript string (for an onClick handler) which will load the EditDocumentController script that shows the form for editing of the record(s) you have send as params.
2502
     * REMEMBER to always htmlspecialchar() content in href-properties to ampersands get converted to entities (XHTML requirement and XSS precaution)
2503
     *
2504
     * @param string $params Parameters sent along to EditDocumentController. This requires a much more details description which you must seek in Inside TYPO3s documentation of the FormEngine API. And example could be '&edit[pages][123] = edit' which will show edit form for page record 123.
2505
     * @param string $_ (unused)
2506
     * @param string $requestUri An optional returnUrl you can set - automatically set to REQUEST_URI.
2507
     *
2508
     * @return string
2509
     */
2510
    public static function editOnClick($params, $_ = '', $requestUri = '')
0 ignored issues
show
Unused Code introduced by
The parameter $_ is not used and could be removed. ( Ignorable by Annotation )

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

2510
    public static function editOnClick($params, /** @scrutinizer ignore-unused */ $_ = '', $requestUri = '')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2511
    {
2512
        if ($requestUri == -1) {
2513
            $returnUrl = 'T3_THIS_LOCATION';
2514
        } else {
2515
            $returnUrl = GeneralUtility::quoteJSvalue(rawurlencode($requestUri ?: GeneralUtility::getIndpEnv('REQUEST_URI')));
2516
        }
2517
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2518
        return 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('record_edit') . $params . '&returnUrl=') . '+' . $returnUrl . '; return false;';
2519
    }
2520
2521
    /**
2522
     * Returns a JavaScript string for viewing the page id, $id
2523
     * It will detect the correct domain name if needed and provide the link with the right back path.
2524
     * Also it will re-use any window already open.
2525
     *
2526
     * @param int $pageUid Page UID
2527
     * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2528
     * @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)
2529
     * @param string $anchorSection Optional anchor to the URL
2530
     * @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!
2531
     * @param string $additionalGetVars Additional GET variables.
2532
     * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2533
     * @return string
2534
     */
2535
    public static function viewOnClick(
2536
        $pageUid,
2537
        $backPath = '',
2538
        $rootLine = null,
2539
        $anchorSection = '',
2540
        $alternativeUrl = '',
2541
        $additionalGetVars = '',
2542
        $switchFocus = true
2543
    ) {
2544
        $viewScript = '/index.php?id=';
2545
        if ($alternativeUrl) {
2546
            $viewScript = $alternativeUrl;
2547
        }
2548
2549
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2550
            $hookObj = GeneralUtility::makeInstance($className);
2551
            if (method_exists($hookObj, 'preProcess')) {
2552
                $hookObj->preProcess(
2553
                    $pageUid,
2554
                    $backPath,
2555
                    $rootLine,
2556
                    $anchorSection,
2557
                    $viewScript,
2558
                    $additionalGetVars,
2559
                    $switchFocus
2560
                );
2561
            }
2562
        }
2563
2564
        if ($alternativeUrl) {
2565
            $previewUrl = $viewScript;
2566
        } else {
2567
            $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2568
            $pageInfo = self::readPageAccess($pageUid, $permissionClause);
2569
            $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo);
2570
            $previewUrl = self::createPreviewUrl($pageUid, $rootLine, $anchorSection, $additionalGetVars, $viewScript);
2571
        }
2572
2573
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2574
            $hookObj = GeneralUtility::makeInstance($className);
2575
            if (method_exists($hookObj, 'postProcess')) {
2576
                $previewUrl = $hookObj->postProcess(
2577
                    $previewUrl,
2578
                    $pageUid,
2579
                    $rootLine,
2580
                    $anchorSection,
2581
                    $viewScript,
2582
                    $additionalGetVars,
2583
                    $switchFocus
2584
                );
2585
            }
2586
        }
2587
2588
        $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');' . ($switchFocus ? 'previewWin.focus();' : '') . LF
2589
            . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };';
2590
        return $onclickCode;
2591
    }
2592
2593
    /**
2594
     * Makes click menu link (context sensitive menu)
2595
     *
2596
     * Returns $str wrapped in a link which will activate the context sensitive
2597
     * menu for the record ($table/$uid) or file ($table = file)
2598
     * The link will load the top frame with the parameter "&item" which is the table, uid
2599
     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2600
     *
2601
     * @param string $content String to be wrapped in link, typ. image tag.
2602
     * @param string $table Table name/File path. If the icon is for a database
2603
     * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2604
     * the absolute filepath
2605
     * @param int|string $uid If icon is for database record this is the UID for the
2606
     * record from $table or identifier for sys_file record
2607
     * @param string $context Set tree if menu is called from tree view
2608
     * @param string $_addParams NOT IN USE
2609
     * @param string $_enDisItems NOT IN USE
2610
     * @param bool $returnTagParameters If set, will return only the onclick
2611
     * JavaScript, not the whole link.
2612
     *
2613
     * @return string The link wrapped input string.
2614
     */
2615
    public static function wrapClickMenuOnIcon(
2616
        $content,
2617
        $table,
2618
        $uid = 0,
2619
        $context = '',
2620
        $_addParams = '',
0 ignored issues
show
Unused Code introduced by
The parameter $_addParams is not used and could be removed. ( Ignorable by Annotation )

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

2620
        /** @scrutinizer ignore-unused */ $_addParams = '',

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2621
        $_enDisItems = '',
0 ignored issues
show
Unused Code introduced by
The parameter $_enDisItems is not used and could be removed. ( Ignorable by Annotation )

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

2621
        /** @scrutinizer ignore-unused */ $_enDisItems = '',

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2622
        $returnTagParameters = false
2623
    ) {
2624
        $tagParameters = [
2625
            'class' => 't3js-contextmenutrigger',
2626
            'data-table' => $table,
2627
            'data-uid' => $uid,
2628
            'data-context' => $context
2629
        ];
2630
2631
        if ($returnTagParameters) {
2632
            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...
2633
        }
2634
        return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2635
    }
2636
2637
    /**
2638
     * Returns a URL with a command to TYPO3 Datahandler
2639
     *
2640
     * @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
2641
     * @param string|int $redirectUrl Redirect URL, default is to use GeneralUtility::getIndpEnv('REQUEST_URI'), -1 means to generate an URL for JavaScript using T3_THIS_LOCATION
2642
     * @return string URL to BackendUtility::getModuleUrl('tce_db') + parameters
2643
     */
2644
    public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2645
    {
2646
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2647
        $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2648
        if ((int)$redirectUrl === -1) {
2649
            $url = GeneralUtility::quoteJSvalue($url) . '+T3_THIS_LOCATION';
2650
        } else {
2651
            $url .= rawurlencode($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI'));
2652
        }
2653
        return $url;
2654
    }
2655
2656
    /**
2657
     * Creates the view-on-click preview URL without any alternative URL.
2658
     *
2659
     * @param int $pageUid Page UID
2660
     * @param array $rootLine If rootline is supplied, the function will look for the first found domain record and use that URL instead
2661
     * @param string $anchorSection Optional anchor to the URL
2662
     * @param string $additionalGetVars Additional GET variables.
2663
     * @param string $viewScript The path to the script used to view the page
2664
     *
2665
     * @return string The preview URL
2666
     */
2667
    protected static function createPreviewUrl($pageUid, $rootLine, $anchorSection, $additionalGetVars, $viewScript)
2668
    {
2669
        // Look if a fixed preview language should be added:
2670
        $beUser = static::getBackendUserAuthentication();
2671
        $viewLanguageOrder = $beUser->getTSConfigVal('options.view.languageOrder');
2672
2673
        if ((string)$viewLanguageOrder !== '') {
2674
            $suffix = '';
2675
            // Find allowed languages (if none, all are allowed!)
2676
            $allowedLanguages = null;
2677
            if (!$beUser->isAdmin() && $beUser->groupData['allowed_languages'] !== '') {
2678
                $allowedLanguages = array_flip(explode(',', $beUser->groupData['allowed_languages']));
2679
            }
2680
            // Traverse the view order, match first occurrence:
2681
            $languageOrder = GeneralUtility::intExplode(',', $viewLanguageOrder);
2682
            foreach ($languageOrder as $langUid) {
2683
                if (is_array($allowedLanguages) && !empty($allowedLanguages)) {
2684
                    // Choose if set.
2685
                    if (isset($allowedLanguages[$langUid])) {
2686
                        $suffix = '&L=' . $langUid;
2687
                        break;
2688
                    }
2689
                } else {
2690
                    // All allowed since no lang. are listed.
2691
                    $suffix = '&L=' . $langUid;
2692
                    break;
2693
                }
2694
            }
2695
            // Add it
2696
            $additionalGetVars .= $suffix;
2697
        }
2698
2699
        // Check a mount point needs to be previewed
2700
        $sys_page = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
2701
        $sys_page->init(false);
2702
        $mountPointInfo = $sys_page->getMountPointInfo($pageUid);
2703
2704
        if ($mountPointInfo && $mountPointInfo['overlay']) {
2705
            $pageUid = $mountPointInfo['mount_pid'];
2706
            $additionalGetVars .= '&MP=' . $mountPointInfo['MPvar'];
2707
        }
2708
        $viewDomain = self::getViewDomain($pageUid, $rootLine);
2709
2710
        return $viewDomain . $viewScript . $pageUid . $additionalGetVars . $anchorSection;
2711
    }
2712
2713
    /**
2714
     * Builds the frontend view domain for a given page ID with a given root
2715
     * line.
2716
     *
2717
     * @param int $pageId The page ID to use, must be > 0
2718
     * @param array|null $rootLine The root line structure to use
2719
     * @return string The full domain including the protocol http:// or https://, but without the trailing '/'
2720
     */
2721
    public static function getViewDomain($pageId, $rootLine = null)
2722
    {
2723
        $domain = rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), '/');
2724
        if (!is_array($rootLine)) {
2725
            $rootLine = self::BEgetRootLine($pageId);
2726
        }
2727
        // Checks alternate domains
2728
        if (!empty($rootLine)) {
2729
            $urlParts = parse_url($domain);
2730
            $protocol = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
2731
            $previewDomainConfig = static::getBackendUserAuthentication()->getTSConfig(
2732
                'TCEMAIN.previewDomain',
2733
                self::getPagesTSconfig($pageId)
2734
            );
2735
            if ($previewDomainConfig['value']) {
2736
                if (strpos($previewDomainConfig['value'], '://') !== false) {
2737
                    list($protocol, $domainName) = explode('://', $previewDomainConfig['value']);
2738
                } else {
2739
                    $domainName = $previewDomainConfig['value'];
2740
                }
2741
            } else {
2742
                $domainName = self::firstDomainRecord($rootLine);
2743
            }
2744
            if ($domainName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $domainName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2745
                $domain = $domainName;
2746
            } else {
2747
                $domainRecord = self::getDomainStartPage($urlParts['host'], $urlParts['path']);
2748
                $domain = $domainRecord['domainName'];
2749
            }
2750
            if ($domain) {
2751
                $domain = $protocol . '://' . $domain;
2752
            } else {
2753
                $domain = rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), '/');
2754
            }
2755
            // Append port number if lockSSLPort is not the standard port 443
2756
            $portNumber = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort'];
2757
            if ($portNumber > 0 && $portNumber !== 443 && $portNumber < 65536 && $protocol === 'https') {
2758
                $domain .= ':' . strval($portNumber);
2759
            }
2760
        }
2761
        return $domain;
2762
    }
2763
2764
    /**
2765
     * Returns the merged User/Page TSconfig for page id, $id.
2766
     * Please read details about module programming elsewhere!
2767
     *
2768
     * @param int $id Page uid
2769
     * @param string $TSref An object string which determines the path of the TSconfig to return.
2770
     * @return array
2771
     */
2772
    public static function getModTSconfig($id, $TSref)
2773
    {
2774
        $beUser = static::getBackendUserAuthentication();
2775
        $pageTS_modOptions = $beUser->getTSConfig($TSref, static::getPagesTSconfig($id));
2776
        $BE_USER_modOptions = $beUser->getTSConfig($TSref);
2777
        if (is_null($BE_USER_modOptions['value'])) {
2778
            unset($BE_USER_modOptions['value']);
2779
        }
2780
        ArrayUtility::mergeRecursiveWithOverrule($pageTS_modOptions, $BE_USER_modOptions);
2781
        return $pageTS_modOptions;
2782
    }
2783
2784
    /**
2785
     * Returns a selector box "function menu" for a module
2786
     * Requires the JS function jumpToUrl() to be available
2787
     * See Inside TYPO3 for details about how to use / make Function menus
2788
     *
2789
     * @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=...
2790
     * @param string $elementName The form elements name, probably something like "SET[...]
2791
     * @param string $currentValue The value to be selected currently.
2792
     * @param array $menuItems An array with the menu items for the selector box
2793
     * @param string $script The script to send the &id to, if empty it's automatically found
2794
     * @param string $addParams Additional parameters to pass to the script.
2795
     * @return string HTML code for selector box
2796
     */
2797
    public static function getFuncMenu(
2798
        $mainParams,
2799
        $elementName,
2800
        $currentValue,
2801
        $menuItems,
2802
        $script = '',
2803
        $addParams = ''
2804
    ) {
2805
        if (!is_array($menuItems) || count($menuItems) <= 1) {
2806
            return '';
2807
        }
2808
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2809
        $options = [];
2810
        foreach ($menuItems as $value => $label) {
2811
            $options[] = '<option value="'
2812
                . htmlspecialchars($value) . '"'
2813
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2814
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2815
        }
2816
        if (!empty($options)) {
2817
            $onChange = 'jumpToUrl(' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+this.options[this.selectedIndex].value,this);';
2818
            return '
2819
2820
				<!-- Function Menu of module -->
2821
				<select class="form-control" name="' . $elementName . '" onchange="' . htmlspecialchars($onChange) . '">
2822
					' . implode('
2823
					', $options) . '
2824
				</select>
2825
						';
2826
        }
2827
        return '';
2828
    }
2829
2830
    /**
2831
     * Returns a selector box to switch the view
2832
     * Requires the JS function jumpToUrl() to be available
2833
     * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose.
2834
     * Mingling with getFuncMenu would harm the docHeader Menu.
2835
     *
2836
     * @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=...
2837
     * @param string $elementName The form elements name, probably something like "SET[...]
2838
     * @param string $currentValue The value to be selected currently.
2839
     * @param array $menuItems An array with the menu items for the selector box
2840
     * @param string $script The script to send the &id to, if empty it's automatically found
2841
     * @param string $addParams Additional parameters to pass to the script.
2842
     * @return string HTML code for selector box
2843
     */
2844
    public static function getDropdownMenu(
2845
        $mainParams,
2846
        $elementName,
2847
        $currentValue,
2848
        $menuItems,
2849
        $script = '',
2850
        $addParams = ''
2851
    ) {
2852
        if (!is_array($menuItems) || count($menuItems) <= 1) {
2853
            return '';
2854
        }
2855
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2856
        $options = [];
2857
        foreach ($menuItems as $value => $label) {
2858
            $options[] = '<option value="'
2859
                . htmlspecialchars($value) . '"'
2860
                . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>'
2861
                . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>';
2862
        }
2863
        if (!empty($options)) {
2864
            $onChange = 'jumpToUrl(' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+this.options[this.selectedIndex].value,this);';
2865
            return '
2866
			<div class="form-group">
2867
				<!-- Function Menu of module -->
2868
				<select class="form-control input-sm" name="' . htmlspecialchars($elementName) . '" onchange="' . htmlspecialchars($onChange) . '">
2869
					' . implode(LF, $options) . '
2870
				</select>
2871
			</div>
2872
						';
2873
        }
2874
        return '';
2875
    }
2876
2877
    /**
2878
     * Checkbox function menu.
2879
     * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox.
2880
     *
2881
     * @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=...
2882
     * @param string $elementName The form elements name, probably something like "SET[...]
2883
     * @param string $currentValue The value to be selected currently.
2884
     * @param string $script The script to send the &id to, if empty it's automatically found
2885
     * @param string $addParams Additional parameters to pass to the script.
2886
     * @param string $tagParams Additional attributes for the checkbox input tag
2887
     * @return string HTML code for checkbox
2888
     * @see getFuncMenu()
2889
     */
2890
    public static function getFuncCheck(
2891
        $mainParams,
2892
        $elementName,
2893
        $currentValue,
2894
        $script = '',
2895
        $addParams = '',
2896
        $tagParams = ''
2897
    ) {
2898
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2899
        $onClick = 'jumpToUrl(' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+(this.checked?1:0),this);';
2900
2901
        return
2902
            '<input' .
2903
            ' type="checkbox"' .
2904
            ' class="checkbox"' .
2905
            ' name="' . $elementName . '"' .
2906
            ($currentValue ? ' checked="checked"' : '') .
2907
            ' onclick="' . htmlspecialchars($onClick) . '"' .
2908
            ($tagParams ? ' ' . $tagParams : '') .
2909
            ' value="1"' .
2910
            ' />';
2911
    }
2912
2913
    /**
2914
     * Input field function menu
2915
     * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange"
2916
     *
2917
     * @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=...
2918
     * @param string $elementName The form elements name, probably something like "SET[...]
2919
     * @param string $currentValue The value to be selected currently.
2920
     * @param int $size Relative size of input field, max is 48
2921
     * @param string $script The script to send the &id to, if empty it's automatically found
2922
     * @param string $addParams Additional parameters to pass to the script.
2923
     * @return string HTML code for input text field.
2924
     * @see getFuncMenu()
2925
     */
2926
    public static function getFuncInput(
2927
        $mainParams,
2928
        $elementName,
2929
        $currentValue,
2930
        $size = 10,
0 ignored issues
show
Unused Code introduced by
The parameter $size is not used and could be removed. ( Ignorable by Annotation )

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

2930
        /** @scrutinizer ignore-unused */ $size = 10,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2931
        $script = '',
2932
        $addParams = ''
2933
    ) {
2934
        $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script);
2935
        $onChange = 'jumpToUrl(' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value),this);';
2936
        return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />';
2937
    }
2938
2939
    /**
2940
     * Builds the URL to the current script with given arguments
2941
     *
2942
     * @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=...
2943
     * @param string $addParams Additional parameters to pass to the script.
2944
     * @param string $script The script to send the &id to, if empty it's automatically found
2945
     * @return string The completes script URL
2946
     */
2947
    protected static function buildScriptUrl($mainParams, $addParams, $script = '')
2948
    {
2949
        if (!is_array($mainParams)) {
2950
            $mainParams = ['id' => $mainParams];
2951
        }
2952
        if (!$script) {
2953
            $script = basename(PATH_thisScript);
2954
        }
2955
2956
        if ($routePath = GeneralUtility::_GP('route')) {
2957
            $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
2958
            $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
2959
            $scriptUrl .= $addParams;
2960
        } else {
2961
            $scriptUrl = $script . '?' . GeneralUtility::implodeArrayForUrl('', $mainParams) . $addParams;
2962
        }
2963
2964
        return $scriptUrl;
2965
    }
2966
2967
    /**
2968
     * Removes menu items from $itemArray if they are configured to be removed by TSconfig for the module ($modTSconfig)
2969
     * See Inside TYPO3 about how to program modules and use this API.
2970
     *
2971
     * @param array $modTSconfig Module TS config array
2972
     * @param array $itemArray Array of items from which to remove items.
2973
     * @param string $TSref $TSref points to the "object string" in $modTSconfig
2974
     * @return array The modified $itemArray is returned.
2975
     */
2976
    public static function unsetMenuItems($modTSconfig, $itemArray, $TSref)
2977
    {
2978
        // Getting TS-config options for this module for the Backend User:
2979
        $conf = static::getBackendUserAuthentication()->getTSConfig($TSref, $modTSconfig);
2980
        if (is_array($conf['properties'])) {
2981
            foreach ($conf['properties'] as $key => $val) {
2982
                if (!$val) {
2983
                    unset($itemArray[$key]);
2984
                }
2985
            }
2986
        }
2987
        return $itemArray;
2988
    }
2989
2990
    /**
2991
     * Call to update the page tree frame (or something else..?) after
2992
     * use 'updatePageTree' as a first parameter will set the page tree to be updated.
2993
     *
2994
     * @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.
2995
     * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree
2996
     * @see BackendUtility::getUpdateSignalCode()
2997
     */
2998
    public static function setUpdateSignal($set = '', $params = '')
2999
    {
3000
        $beUser = static::getBackendUserAuthentication();
3001
        $modData = $beUser->getModuleData(
3002
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
3003
            'ses'
3004
        );
3005
        if ($set) {
3006
            $modData[$set] = [
3007
                'set' => $set,
3008
                'parameter' => $params
3009
            ];
3010
        } else {
3011
            // clear the module data
3012
            $modData = [];
3013
        }
3014
        $beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData);
3015
    }
3016
3017
    /**
3018
     * Call to update the page tree frame (or something else..?) if this is set by the function
3019
     * setUpdateSignal(). It will return some JavaScript that does the update
3020
     *
3021
     * @return string HTML javascript code
3022
     * @see BackendUtility::setUpdateSignal()
3023
     */
3024
    public static function getUpdateSignalCode()
3025
    {
3026
        $signals = [];
3027
        $modData = static::getBackendUserAuthentication()->getModuleData(
3028
            \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal',
3029
            'ses'
3030
        );
3031
        if (empty($modData)) {
3032
            return '';
3033
        }
3034
        // Hook: Allows to let TYPO3 execute your JS code
3035
        $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? [];
3036
        // Loop through all setUpdateSignals and get the JS code
3037
        foreach ($modData as $set => $val) {
3038
            if (isset($updateSignals[$set])) {
3039
                $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => ''];
3040
                $ref = null;
3041
                GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref);
3042
                $signals[] = $params['JScode'];
3043
            } else {
3044
                switch ($set) {
3045
                    case 'updatePageTree':
3046
                        $signals[] = '
3047
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) {
3048
									top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
3049
								}
3050
							';
3051
                        break;
3052
                    case 'updateFolderTree':
3053
                        $signals[] = '
3054
								if (top && top.nav_frame && top.nav_frame.location) {
3055
									top.nav_frame.location.reload(true);
3056
								}';
3057
                        break;
3058
                    case 'updateModuleMenu':
3059
                        $signals[] = '
3060
								if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) {
3061
									top.TYPO3.ModuleMenu.App.refreshMenu();
3062
								}';
3063
                        break;
3064
                    case 'updateTopbar':
3065
                        $signals[] = '
3066
								if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) {
3067
									top.TYPO3.Backend.Topbar.refresh();
3068
								}';
3069
                        break;
3070
                }
3071
            }
3072
        }
3073
        $content = implode(LF, $signals);
3074
        // For backwards compatibility, should be replaced
3075
        self::setUpdateSignal();
3076
        return $content;
3077
    }
3078
3079
    /**
3080
     * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module.
3081
     * This is kind of session variable management framework for the backend users.
3082
     * 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
3083
     * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules.
3084
     *
3085
     * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus.
3086
     * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus.
3087
     * @param string $modName modName is the name of this module. Used to get the correct module data.
3088
     * @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.
3089
     * @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.
3090
     * @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)
3091
     * @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
3092
     */
3093
    public static function getModuleData(
3094
        $MOD_MENU,
3095
        $CHANGED_SETTINGS,
3096
        $modName,
3097
        $type = '',
3098
        $dontValidateList = '',
3099
        $setDefaultList = ''
3100
    ) {
3101
        if ($modName && is_string($modName)) {
3102
            // Getting stored user-data from this module:
3103
            $beUser = static::getBackendUserAuthentication();
3104
            $settings = $beUser->getModuleData($modName, $type);
3105
            $changed = 0;
3106
            if (!is_array($settings)) {
3107
                $changed = 1;
3108
                $settings = [];
3109
            }
3110
            if (is_array($MOD_MENU)) {
3111
                foreach ($MOD_MENU as $key => $var) {
3112
                    // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array.
3113
                    if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) {
3114
                        if (is_array($CHANGED_SETTINGS[$key])) {
3115
                            $serializedSettings = serialize($CHANGED_SETTINGS[$key]);
3116
                            if ((string)$settings[$key] !== $serializedSettings) {
3117
                                $settings[$key] = $serializedSettings;
3118
                                $changed = 1;
3119
                            }
3120
                        } else {
3121
                            if ((string)$settings[$key] !== (string)$CHANGED_SETTINGS[$key]) {
3122
                                $settings[$key] = $CHANGED_SETTINGS[$key];
3123
                                $changed = 1;
3124
                            }
3125
                        }
3126
                    }
3127
                    // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted
3128
                    if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) {
3129
                        // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted.
3130
                        if (is_array($settings[$key]) || !isset($MOD_MENU[$key][$settings[$key]])) {
3131
                            $settings[$key] = (string)key($var);
3132
                            $changed = 1;
3133
                        }
3134
                    }
3135
                    // Sets default values (only strings/checkboxes, not menus)
3136
                    if ($setDefaultList && !is_array($var)) {
3137
                        if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) {
3138
                            $settings[$key] = (string)$var;
3139
                        }
3140
                    }
3141
                }
3142
            } else {
3143
                die('No menu!');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3144
            }
3145
            if ($changed) {
3146
                $beUser->pushModuleData($modName, $settings);
3147
            }
3148
            return $settings;
3149
        }
3150
        die('Wrong module name: "' . $modName . '"');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3151
    }
3152
3153
    /**
3154
     * Returns the URL to a given module
3155
     *
3156
     * @param string $moduleName Name of the module
3157
     * @param array $urlParameters URL parameters that should be added as key value pairs
3158
     * @return string Calculated URL
3159
     */
3160
    public static function getModuleUrl($moduleName, $urlParameters = [])
3161
    {
3162
        /** @var UriBuilder $uriBuilder */
3163
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
3164
        try {
3165
            $uri = $uriBuilder->buildUriFromRoute($moduleName, $urlParameters);
3166
        } catch (\TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException $e) {
3167
            $uri = $uriBuilder->buildUriFromRoutePath($moduleName, $urlParameters);
3168
        }
3169
        return (string)$uri;
3170
    }
3171
3172
    /*******************************************
3173
     *
3174
     * Core
3175
     *
3176
     *******************************************/
3177
    /**
3178
     * Unlock or Lock a record from $table with $uid
3179
     * If $table and $uid is not set, then all locking for the current BE_USER is removed!
3180
     *
3181
     * @param string $table Table name
3182
     * @param int $uid Record uid
3183
     * @param int $pid Record pid
3184
     * @internal
3185
     */
3186
    public static function lockRecords($table = '', $uid = 0, $pid = 0)
3187
    {
3188
        $beUser = static::getBackendUserAuthentication();
3189
        if (isset($beUser->user['uid'])) {
3190
            $userId = (int)$beUser->user['uid'];
3191
            if ($table && $uid) {
3192
                $fieldsValues = [
3193
                    'userid' => $userId,
3194
                    'feuserid' => 0,
3195
                    'tstamp' => $GLOBALS['EXEC_TIME'],
3196
                    'record_table' => $table,
3197
                    'record_uid' => $uid,
3198
                    'username' => $beUser->user['username'],
3199
                    'record_pid' => $pid
3200
                ];
3201
                GeneralUtility::makeInstance(ConnectionPool::class)
3202
                    ->getConnectionForTable('sys_lockedrecords')
3203
                    ->insert(
3204
                        'sys_lockedrecords',
3205
                        $fieldsValues
3206
                    );
3207
            } else {
3208
                GeneralUtility::makeInstance(ConnectionPool::class)
3209
                    ->getConnectionForTable('sys_lockedrecords')
3210
                    ->delete(
3211
                        'sys_lockedrecords',
3212
                        ['userid' => (int)$userId]
3213
                    );
3214
            }
3215
        }
3216
    }
3217
3218
    /**
3219
     * Returns information about whether the record from table, $table, with uid, $uid is currently locked
3220
     * (edited by another user - which should issue a warning).
3221
     * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts
3222
     * are activated - which means that a user CAN have a record "open" without having it locked.
3223
     * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient.
3224
     *
3225
     * @param string $table Table name
3226
     * @param int $uid Record uid
3227
     * @return array|bool
3228
     * @internal
3229
     */
3230
    public static function isRecordLocked($table, $uid)
3231
    {
3232
        $runtimeCache = self::getRuntimeCache();
3233
        $cacheId = 'backend-recordLocked';
3234
        $recordLockedCache = $runtimeCache->get($cacheId);
3235
        if ($recordLockedCache !== false) {
3236
            $lockedRecords = $recordLockedCache;
3237
        } else {
3238
            $lockedRecords = [];
3239
3240
            $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords');
3241
            $result = $queryBuilder
3242
                ->select('*')
3243
                ->from('sys_lockedrecords')
3244
                ->where(
3245
                    $queryBuilder->expr()->neq(
3246
                        'sys_lockedrecords.userid',
3247
                        $queryBuilder->createNamedParameter(
3248
                            static::getBackendUserAuthentication()->user['uid'],
3249
                            \PDO::PARAM_INT
3250
                        )
3251
                    ),
3252
                    $queryBuilder->expr()->gt(
3253
                        'sys_lockedrecords.tstamp',
3254
                        $queryBuilder->createNamedParameter(
3255
                            ($GLOBALS['EXEC_TIME'] - 2 * 3600),
3256
                            \PDO::PARAM_INT
3257
                        )
3258
                    )
3259
                )
3260
                ->execute();
3261
3262
            $lang = static::getLanguageService();
3263
            while ($row = $result->fetch()) {
3264
                // Get the type of the user that locked this record:
3265
                if ($row['userid']) {
3266
                    $userTypeLabel = 'beUser';
3267
                } elseif ($row['feuserid']) {
3268
                    $userTypeLabel = 'feUser';
3269
                } else {
3270
                    $userTypeLabel = 'user';
3271
                }
3272
                $userType = $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel);
3273
                // Get the username (if available):
3274
                if ($row['username']) {
3275
                    $userName = $row['username'];
3276
                } else {
3277
                    $userName = $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.unknownUser');
3278
                }
3279
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row;
3280
                $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf(
3281
                    $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'),
3282
                    $userType,
3283
                    $userName,
3284
                    self::calcAge(
3285
                        $GLOBALS['EXEC_TIME'] - $row['tstamp'],
3286
                        $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
3287
                    )
3288
                );
3289
                if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) {
3290
                    $lockedRecords['pages:' . $row['record_pid']]['msg'] = sprintf(
3291
                        $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'),
3292
                        $userType,
3293
                        $userName,
3294
                        self::calcAge(
3295
                            $GLOBALS['EXEC_TIME'] - $row['tstamp'],
3296
                            $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
3297
                        )
3298
                    );
3299
                }
3300
            }
3301
            $runtimeCache->set($cacheId, $lockedRecords);
3302
        }
3303
3304
        return $lockedRecords[$table . ':' . $uid] ?? false;
3305
    }
3306
3307
    /**
3308
     * Returns TSConfig for the TCEFORM object in Page TSconfig.
3309
     * Used in TCEFORMs
3310
     *
3311
     * @param string $table Table name present in TCA
3312
     * @param array $row Row from table
3313
     * @return array
3314
     */
3315
    public static function getTCEFORM_TSconfig($table, $row)
3316
    {
3317
        self::fixVersioningPid($table, $row);
3318
        $res = [];
3319
        // Get main config for the table
3320
        list($TScID, $cPid) = self::getTSCpid($table, $row['uid'], $row['pid']);
3321
        if ($TScID >= 0) {
3322
            $tempConf = static::getBackendUserAuthentication()->getTSConfig(
3323
                'TCEFORM.' . $table,
3324
                self::getPagesTSconfig($TScID)
3325
            );
3326
            if (is_array($tempConf['properties'])) {
3327
                $typeVal = self::getTCAtypeValue($table, $row);
3328
                foreach ($tempConf['properties'] as $key => $val) {
3329
                    if (is_array($val)) {
3330
                        $fieldN = substr($key, 0, -1);
3331
                        $res[$fieldN] = $val;
3332
                        unset($res[$fieldN]['types.']);
3333
                        if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'])) {
3334
                            ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']);
3335
                        }
3336
                    }
3337
                }
3338
            }
3339
        }
3340
        $res['_CURRENT_PID'] = $cPid;
3341
        $res['_THIS_UID'] = $row['uid'];
3342
        // So the row will be passed to foreign_table_where_query()
3343
        $res['_THIS_ROW'] = $row;
3344
        return $res;
3345
    }
3346
3347
    /**
3348
     * Find the real PID of the record (with $uid from $table).
3349
     * 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).
3350
     * NOTICE: Make sure that the input PID is never negative because the record was an offline version!
3351
     * Therefore, you should always use BackendUtility::fixVersioningPid($table,$row); on the data you input before calling this function!
3352
     *
3353
     * @param string $table Table name
3354
     * @param int $uid Record uid
3355
     * @param int $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return
3356
     * @return int
3357
     * @internal
3358
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord(), getTSCpid()
3359
     */
3360
    public static function getTSconfig_pidValue($table, $uid, $pid)
3361
    {
3362
        // If pid is an integer this takes precedence in our lookup.
3363
        if (MathUtility::canBeInterpretedAsInteger($pid)) {
3364
            $thePidValue = (int)$pid;
3365
            // If ref to another record, look that record up.
3366
            if ($thePidValue < 0) {
3367
                $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

3367
                $pidRec = self::getRecord($table, /** @scrutinizer ignore-type */ abs($thePidValue), 'pid');
Loading history...
3368
                $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2;
3369
            }
3370
        } else {
3371
            // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing
3372
            $rr = self::getRecord($table, $uid);
3373
            $thePidValue = null;
3374
            if (is_array($rr)) {
3375
                // First check if the pid is -1 which means it is a workspaced element. Get the "real" record:
3376
                if ($rr['pid'] == '-1') {
3377
                    $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid');
3378
                    if (is_array($rr)) {
3379
                        $thePidValue = $rr['pid'];
3380
                    }
3381
                } else {
3382
                    // Returning the "pid" of the record
3383
                    $thePidValue = $rr['pid'];
3384
                }
3385
            }
3386
            if (!$thePidValue) {
3387
                // Returns -1 if the record with this pid was not found.
3388
                $thePidValue = -1;
3389
            }
3390
        }
3391
        return $thePidValue;
3392
    }
3393
3394
    /**
3395
     * Return $uid if $table is pages and $uid is int - otherwise the $pid
3396
     *
3397
     * @param string $table Table name
3398
     * @param int $uid Record uid
3399
     * @param int $pid Record pid
3400
     * @return int
3401
     * @internal
3402
     */
3403
    public static function getPidForModTSconfig($table, $uid, $pid)
3404
    {
3405
        return $table === 'pages' && MathUtility::canBeInterpretedAsInteger($uid) ? $uid : $pid;
3406
    }
3407
3408
    /**
3409
     * Return the real pid of a record and caches the result.
3410
     * The non-cached method needs database queries to do the job, so this method
3411
     * can be used if code sometimes calls the same record multiple times to save
3412
     * some queries. This should not be done if the calling code may change the
3413
     * same record meanwhile.
3414
     *
3415
     * @param string $table Tablename
3416
     * @param string $uid UID value
3417
     * @param string $pid PID value
3418
     * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig.
3419
     */
3420
    public static function getTSCpidCached($table, $uid, $pid)
3421
    {
3422
        // A local first level cache
3423
        static $firstLevelCache;
3424
3425
        if (!is_array($firstLevelCache)) {
3426
            $firstLevelCache = [];
3427
        }
3428
3429
        $key = $table . ':' . $uid . ':' . $pid;
3430
        if (!isset($firstLevelCache[$key])) {
3431
            $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

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

3431
            $firstLevelCache[$key] = static::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ $pid);
Loading history...
3432
        }
3433
        return $firstLevelCache[$key];
3434
    }
3435
3436
    /**
3437
     * 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.
3438
     *
3439
     * @param string $table Table name
3440
     * @param int $uid Record uid
3441
     * @param int $pid Record pid
3442
     * @return array Array of two ints; first is the REAL PID of a record and if its a new record negative values are resolved to the true PID,
3443
     * second value is the PID value for TSconfig (uid if table is pages, otherwise the pid)
3444
     * @internal
3445
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::setHistory(), \TYPO3\CMS\Core\DataHandling\DataHandler::process_datamap()
3446
     */
3447
    public static function getTSCpid($table, $uid, $pid)
3448
    {
3449
        // If pid is negative (referring to another record) the pid of the other record is fetched and returned.
3450
        $cPid = self::getTSconfig_pidValue($table, $uid, $pid);
3451
        // $TScID is the id of $table = pages, else it's the pid of the record.
3452
        $TScID = self::getPidForModTSconfig($table, $uid, $cPid);
3453
        return [$TScID, $cPid];
3454
    }
3455
3456
    /**
3457
     * Returns first found domain record "domainName" (without trailing slash) if found in the input $rootLine
3458
     *
3459
     * @param array $rootLine Root line array
3460
     * @return string|null Domain name or NULL
3461
     */
3462
    public static function firstDomainRecord($rootLine)
3463
    {
3464
        $queryBuilder = static::getQueryBuilderForTable('sys_domain');
3465
        $queryBuilder->getRestrictions()
3466
            ->removeAll()
3467
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3468
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
3469
3470
        $queryBuilder->select('domainName')
3471
            ->from('sys_domain')
3472
            ->where(
3473
                $queryBuilder->expr()->eq(
3474
                    'pid',
3475
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT, ':pid')
3476
                ),
3477
                $queryBuilder->expr()->eq(
3478
                    'redirectTo',
3479
                    $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
3480
                ),
3481
                $queryBuilder->expr()->eq(
3482
                    'hidden',
3483
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3484
                )
3485
            )
3486
            ->setMaxResults(1)
3487
            ->orderBy('sorting');
3488
3489
        foreach ($rootLine as $row) {
3490
            $domainName = $queryBuilder->setParameter('pid', $row['uid'], \PDO::PARAM_INT)
3491
                ->execute()
3492
                ->fetchColumn(0);
3493
3494
            if ($domainName) {
3495
                return rtrim($domainName, '/');
0 ignored issues
show
Bug introduced by
It seems like $domainName can also be of type true; however, parameter $str of rtrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

3495
                return rtrim(/** @scrutinizer ignore-type */ $domainName, '/');
Loading history...
3496
            }
3497
        }
3498
        return null;
3499
    }
3500
3501
    /**
3502
     * Returns the sys_domain record for $domain, optionally with $path appended.
3503
     *
3504
     * @param string $domain Domain name
3505
     * @param string $path Appended path
3506
     * @return array|bool Domain record, if found, false otherwise
3507
     */
3508
    public static function getDomainStartPage($domain, $path = '')
3509
    {
3510
        $domain = explode(':', $domain);
3511
        $domain = strtolower(preg_replace('/\\.$/', '', $domain[0]));
3512
        // Path is calculated.
3513
        $path = trim(preg_replace('/\\/[^\\/]*$/', '', $path));
3514
        // Stuff
3515
        $domain .= $path;
3516
3517
        $queryBuilder = static::getQueryBuilderForTable('sys_domain');
3518
        $queryBuilder->getRestrictions()
3519
            ->removeAll()
3520
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3521
3522
        $result = $queryBuilder
3523
            ->select('sys_domain.*')
3524
            ->from('sys_domain')
3525
            ->from('pages')
3526
            ->where(
3527
                $queryBuilder->expr()->eq(
3528
                    'sys_domain.pid',
3529
                    $queryBuilder->quoteIdentifier('pages.uid')
3530
                ),
3531
                $queryBuilder->expr()->orX(
3532
                    $queryBuilder->expr()->eq(
3533
                        'sys_domain.domainName',
3534
                        $queryBuilder->createNamedParameter($domain, \PDO::PARAM_STR)
3535
                    ),
3536
                    $queryBuilder->expr()->eq(
3537
                        'sys_domain.domainName',
3538
                        $queryBuilder->createNamedParameter($domain . '/', \PDO::PARAM_STR)
3539
                    )
3540
                )
3541
3542
            )
3543
            ->execute()
3544
            ->fetch();
3545
3546
        return $result;
3547
    }
3548
3549
    /**
3550
     * Returns soft-reference parser for the softRef processing type
3551
     * Usage: $softRefObj = &BackendUtility::softRefParserObj('[parser key]');
3552
     *
3553
     * @param string $spKey softRef parser key
3554
     * @return mixed If available, returns Soft link parser object.
3555
     */
3556
    public static function &softRefParserObj($spKey)
3557
    {
3558
        // If no softRef parser object has been set previously, try to create it:
3559
        if (!isset($GLOBALS['T3_VAR']['softRefParser'][$spKey])) {
3560
            // Set the object string to blank by default:
3561
            $GLOBALS['T3_VAR']['softRefParser'][$spKey] = '';
3562
            // Now, try to create parser object:
3563
            $objRef = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $objRef is dead and can be removed.
Loading history...
3564
            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser'][$spKey])) {
3565
                $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser'][$spKey];
3566
                if ($className) {
3567
                    $GLOBALS['T3_VAR']['softRefParser'][$spKey] = GeneralUtility::makeInstance($className);
3568
                }
3569
            }
3570
        }
3571
        // Return RTE object (if any!)
3572
        return $GLOBALS['T3_VAR']['softRefParser'][$spKey];
3573
    }
3574
3575
    /**
3576
     * Gets an instance of the runtime cache.
3577
     *
3578
     * @return FrontendInterface
3579
     */
3580
    protected static function getRuntimeCache()
3581
    {
3582
        return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
3583
    }
3584
3585
    /**
3586
     * Returns array of soft parser references
3587
     *
3588
     * @param string $parserList softRef parser list
3589
     * @return array|bool Array where the parser key is the key and the value is the parameter string, FALSE if no parsers were found
3590
     * @throws \InvalidArgumentException
3591
     */
3592
    public static function explodeSoftRefParserList($parserList)
3593
    {
3594
        // Return immediately if list is blank:
3595
        if ((string)$parserList === '') {
3596
            return false;
3597
        }
3598
3599
        $runtimeCache = self::getRuntimeCache();
3600
        $cacheId = 'backend-softRefList-' . md5($parserList);
3601
        $parserListCache = $runtimeCache->get($cacheId);
3602
        if ($parserListCache !== false) {
3603
            return $parserListCache;
3604
        }
3605
3606
        // Otherwise parse the list:
3607
        $keyList = GeneralUtility::trimExplode(',', $parserList, true);
3608
        $output = [];
3609
        foreach ($keyList as $val) {
3610
            $reg = [];
3611
            if (preg_match('/^([[:alnum:]_-]+)\\[(.*)\\]$/', $val, $reg)) {
3612
                $output[$reg[1]] = GeneralUtility::trimExplode(';', $reg[2], true);
3613
            } else {
3614
                $output[$val] = '';
3615
            }
3616
        }
3617
        $runtimeCache->set($cacheId, $output);
3618
        return $output;
3619
    }
3620
3621
    /**
3622
     * Returns TRUE if $modName is set and is found as a main- or submodule in $TBE_MODULES array
3623
     *
3624
     * @param string $modName Module name
3625
     * @return bool
3626
     */
3627
    public static function isModuleSetInTBE_MODULES($modName)
3628
    {
3629
        $loaded = [];
3630
        foreach ($GLOBALS['TBE_MODULES'] as $mkey => $list) {
3631
            $loaded[$mkey] = 1;
3632
            if (!is_array($list) && trim($list)) {
3633
                $subList = GeneralUtility::trimExplode(',', $list, true);
3634
                foreach ($subList as $skey) {
3635
                    $loaded[$mkey . '_' . $skey] = 1;
3636
                }
3637
            }
3638
        }
3639
        return $modName && isset($loaded[$modName]);
3640
    }
3641
3642
    /**
3643
     * Counting references to a record/file
3644
     *
3645
     * @param string $table Table name (or "_FILE" if its a file)
3646
     * @param string $ref Reference: If table, then int-uid, if _FILE, then file reference (relative to PATH_site)
3647
     * @param string $msg Message with %s, eg. "There were %s records pointing to this file!
3648
     * @param string|null $count Reference count
3649
     * @return string Output string (or int count value if no msg string specified)
3650
     */
3651
    public static function referenceCount($table, $ref, $msg = '', $count = null)
3652
    {
3653
        if ($count === null) {
3654
3655
            // Build base query
3656
            $queryBuilder = static::getQueryBuilderForTable('sys_refindex');
3657
            $queryBuilder
3658
                ->count('*')
3659
                ->from('sys_refindex')
3660
                ->where(
3661
                    $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
3662
                    $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
3663
                );
3664
3665
            // Look up the path:
3666
            if ($table === '_FILE') {
3667
                if (!GeneralUtility::isFirstPartOfStr($ref, PATH_site)) {
3668
                    return '';
3669
                }
3670
3671
                $ref = PathUtility::stripPathSitePrefix($ref);
3672
                $queryBuilder->andWhere(
3673
                    $queryBuilder->expr()->eq('ref_string', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_STR))
3674
                );
3675
            } else {
3676
                $queryBuilder->andWhere(
3677
                    $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT))
3678
                );
3679
                if ($table === 'sys_file') {
3680
                    $queryBuilder->andWhere($queryBuilder->expr()->neq('tablename', $queryBuilder->quote('sys_file_metadata')));
3681
                }
3682
            }
3683
3684
            $count = $queryBuilder->execute()->fetchColumn(0);
3685
        }
3686
3687
        if ($count) {
3688
            return $msg ? sprintf($msg, $count) : $count;
0 ignored issues
show
Bug introduced by
It seems like $count can also be of type true; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

3688
            return $msg ? sprintf($msg, /** @scrutinizer ignore-type */ $count) : $count;
Loading history...
Bug Best Practice introduced by
The expression return $msg ? sprintf($msg, $count) : $count also could return the type true which is incompatible with the documented return type string.
Loading history...
3689
        }
3690
        return $msg ? '' : 0;
3691
    }
3692
3693
    /**
3694
     * Counting translations of records
3695
     *
3696
     * @param string $table Table name
3697
     * @param string $ref Reference: the record's uid
3698
     * @param string $msg Message with %s, eg. "This record has %s translation(s) which will be deleted, too!
3699
     * @return string Output string (or int count value if no msg string specified)
3700
     */
3701
    public static function translationCount($table, $ref, $msg = '')
3702
    {
3703
        $count = null;
3704
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']
3705
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
3706
        ) {
3707
            $queryBuilder = static::getQueryBuilderForTable($table);
3708
            $queryBuilder->getRestrictions()
3709
                ->removeAll()
3710
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3711
3712
            $count = (int)$queryBuilder
3713
                ->count('*')
3714
                ->from($table)
3715
                ->where(
3716
                    $queryBuilder->expr()->eq(
3717
                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3718
                        $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)
3719
                    ),
3720
                    $queryBuilder->expr()->neq(
3721
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
3722
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3723
                    )
3724
                )
3725
                ->execute()
3726
                ->fetchColumn(0);
3727
        }
3728
3729
        if ($count && $msg) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $count of type null|integer 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...
3730
            return sprintf($msg, $count);
3731
        }
3732
3733
        if ($count) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $count of type null|integer 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...
3734
            return $msg ? sprintf($msg, $count) : $count;
3735
        }
3736
        return $msg ? '' : 0;
3737
    }
3738
3739
    /*******************************************
3740
     *
3741
     * Workspaces / Versioning
3742
     *
3743
     *******************************************/
3744
    /**
3745
     * Select all versions of a record, ordered by version id (DESC)
3746
     *
3747
     * @param string $table Table name to select from
3748
     * @param int $uid Record uid for which to find versions.
3749
     * @param string $fields Field list to select
3750
     * @param int|null $workspace Search in workspace ID and Live WS, if 0 search only in LiveWS, if NULL search in all WS.
3751
     * @param bool $includeDeletedRecords If set, deleted-flagged versions are included! (Only for clean-up script!)
3752
     * @param array $row The current record
3753
     * @return array|null Array of versions of table/uid
3754
     */
3755
    public static function selectVersionsOfRecord(
3756
        $table,
3757
        $uid,
3758
        $fields = '*',
3759
        $workspace = 0,
3760
        $includeDeletedRecords = false,
3761
        $row = null
3762
    ) {
3763
        $realPid = 0;
3764
        $outputRows = [];
3765
        if ($GLOBALS['TCA'][$table] && static::isTableWorkspaceEnabled($table)) {
3766
            if (is_array($row) && !$includeDeletedRecords) {
3767
                $row['_CURRENT_VERSION'] = true;
3768
                $realPid = $row['pid'];
3769
                $outputRows[] = $row;
3770
            } else {
3771
                // Select UID version:
3772
                $row = self::getRecord($table, $uid, $fields, '', !$includeDeletedRecords);
3773
                // Add rows to output array:
3774
                if ($row) {
3775
                    $row['_CURRENT_VERSION'] = true;
3776
                    $realPid = $row['pid'];
3777
                    $outputRows[] = $row;
3778
                }
3779
            }
3780
3781
            $queryBuilder = static::getQueryBuilderForTable($table);
3782
            $queryBuilder->getRestrictions()->removeAll();
3783
3784
            // build fields to select
3785
            $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
3786
3787
            $queryBuilder
3788
                ->from($table)
3789
                ->where(
3790
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
3791
                    $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
3792
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
3793
                )
3794
                ->orderBy('t3ver_id', 'DESC');
3795
3796
            if (!$includeDeletedRecords) {
3797
                $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
3798
            }
3799
3800
            if ($workspace === 0) {
3801
                // Only in Live WS
3802
                $queryBuilder->andWhere(
3803
                    $queryBuilder->expr()->eq(
3804
                        't3ver_wsid',
3805
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3806
                    )
3807
                );
3808
            } elseif ($workspace !== null) {
3809
                // In Live WS and Workspace with given ID
3810
                $queryBuilder->andWhere(
3811
                    $queryBuilder->expr()->in(
3812
                        't3ver_wsid',
3813
                        $queryBuilder->createNamedParameter([0, (int)$workspace], Connection::PARAM_INT_ARRAY)
3814
                    )
3815
                );
3816
            }
3817
3818
            $rows = $queryBuilder->execute()->fetchAll();
3819
3820
            // Add rows to output array:
3821
            if (is_array($rows)) {
3822
                $outputRows = array_merge($outputRows, $rows);
3823
            }
3824
            // Set real-pid:
3825
            foreach ($outputRows as $idx => $oRow) {
3826
                $outputRows[$idx]['_REAL_PID'] = $realPid;
3827
            }
3828
            return $outputRows;
3829
        }
3830
        return null;
3831
    }
3832
3833
    /**
3834
     * Find page-tree PID for versionized record
3835
     * Will look if the "pid" value of the input record is -1 and if the table supports versioning - if so,
3836
     * it will translate the -1 PID into the PID of the original record
3837
     * Used whenever you are tracking something back, like making the root line.
3838
     * Will only translate if the workspace of the input record matches that of the current user (unless flag set)
3839
     * Principle; Record offline! => Find online?
3840
     *
3841
     * If the record had its pid corrected to the online versions pid, then "_ORIG_pid" is set
3842
     * to the original pid value (-1 of course). The field "_ORIG_pid" is used by various other functions
3843
     * to detect if a record was in fact in a versionized branch.
3844
     *
3845
     * @param string $table Table name
3846
     * @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.
3847
     * @param bool $ignoreWorkspaceMatch Ignore workspace match
3848
     * @see PageRepository::fixVersioningPid()
3849
     */
3850
    public static function fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch = false)
3851
    {
3852
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3853
            return;
3854
        }
3855
        // Check that the input record is an offline version from a table that supports versioning:
3856
        if (is_array($rr) && $rr['pid'] == -1 && static::isTableWorkspaceEnabled($table)) {
3857
            // Check values for t3ver_oid and t3ver_wsid:
3858
            if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid'])) {
3859
                // If "t3ver_oid" is already a field, just set this:
3860
                $oid = $rr['t3ver_oid'];
3861
                $wsid = $rr['t3ver_wsid'];
3862
            } else {
3863
                $oid = 0;
3864
                $wsid = 0;
3865
                // Otherwise we have to expect "uid" to be in the record and look up based on this:
3866
                $newPidRec = self::getRecord($table, $rr['uid'], 't3ver_oid,t3ver_wsid');
3867
                if (is_array($newPidRec)) {
3868
                    $oid = $newPidRec['t3ver_oid'];
3869
                    $wsid = $newPidRec['t3ver_wsid'];
3870
                }
3871
            }
3872
            // If ID of current online version is found, look up the PID value of that:
3873
            if ($oid
3874
                && ($ignoreWorkspaceMatch || (int)$wsid === (int)static::getBackendUserAuthentication()->workspace)
3875
            ) {
3876
                $oidRec = self::getRecord($table, $oid, 'pid');
3877
                if (is_array($oidRec)) {
3878
                    $rr['_ORIG_pid'] = $rr['pid'];
3879
                    $rr['pid'] = $oidRec['pid'];
3880
                }
3881
                // Use target PID in case of move pointer
3882
                if (
3883
                    !isset($rr['t3ver_state'])
3884
                    || VersionState::cast($rr['t3ver_state'])->equals(VersionState::MOVE_POINTER)
3885
                ) {
3886
                    $movePlaceholder = self::getMovePlaceholder($table, $oid, 'pid');
3887
                    if ($movePlaceholder) {
3888
                        $rr['_ORIG_pid'] = $rr['pid'];
3889
                        $rr['pid'] = $movePlaceholder['pid'];
3890
                    }
3891
                }
3892
            }
3893
        }
3894
    }
3895
3896
    /**
3897
     * Workspace Preview Overlay
3898
     * Generally ALWAYS used when records are selected based on uid or pid.
3899
     * If records are selected on other fields than uid or pid (eg. "email = ....")
3900
     * then usage might produce undesired results and that should be evaluated on individual basis.
3901
     * Principle; Record online! => Find offline?
3902
     * Recently, this function has been modified so it MAY set $row to FALSE.
3903
     * This happens if a version overlay with the move-id pointer is found in which case we would like a backend preview.
3904
     * In other words, you should check if the input record is still an array afterwards when using this function.
3905
     *
3906
     * @param string $table Table name
3907
     * @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().
3908
     * @param int $wsid Workspace ID, if not specified will use static::getBackendUserAuthentication()->workspace
3909
     * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
3910
     * @see fixVersioningPid()
3911
     */
3912
    public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false)
3913
    {
3914
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
3915
            return;
3916
        }
3917
        // If this is FALSE the placeholder is shown raw in the backend.
3918
        // I don't know if this move can be useful for users to toggle. Technically it can help debugging.
3919
        $previewMovePlaceholders = true;
3920
        // Initialize workspace ID
3921
        if ($wsid == -99) {
3922
            $wsid = static::getBackendUserAuthentication()->workspace;
3923
        }
3924
        // Check if workspace is different from zero and record is set:
3925
        if ($wsid !== 0 && is_array($row)) {
3926
            // Check if input record is a move-placeholder and if so, find the pointed-to live record:
3927
            $movePldSwap = null;
3928
            $orig_uid = 0;
3929
            $orig_pid = 0;
3930
            if ($previewMovePlaceholders) {
3931
                $orig_uid = $row['uid'];
3932
                $orig_pid = $row['pid'];
3933
                $movePldSwap = self::movePlhOL($table, $row);
3934
            }
3935
            $wsAlt = self::getWorkspaceVersionOfRecord(
3936
                $wsid,
3937
                $table,
3938
                $row['uid'],
3939
                implode(',', static::purgeComputedPropertyNames(array_keys($row)))
3940
            );
3941
            // If version was found, swap the default record with that one.
3942
            if (is_array($wsAlt)) {
3943
                // Check if this is in move-state:
3944
                if ($previewMovePlaceholders && !$movePldSwap && static::isTableWorkspaceEnabled($table) && $unsetMovePointers) {
3945
                    // Only for WS ver 2... (moving)
3946
                    // If t3ver_state is not found, then find it... (but we like best if it is here...)
3947
                    if (!isset($wsAlt['t3ver_state'])) {
3948
                        $stateRec = self::getRecord($table, $wsAlt['uid'], 't3ver_state');
3949
                        $versionState = VersionState::cast($stateRec['t3ver_state']);
3950
                    } else {
3951
                        $versionState = VersionState::cast($wsAlt['t3ver_state']);
3952
                    }
3953
                    if ($versionState->equals(VersionState::MOVE_POINTER)) {
3954
                        // @todo Same problem as frontend in versionOL(). See TODO point there.
3955
                        $row = false;
3956
                        return;
3957
                    }
3958
                }
3959
                // Always correct PID from -1 to what it should be
3960
                if (isset($wsAlt['pid'])) {
3961
                    // Keep the old (-1) - indicates it was a version.
3962
                    $wsAlt['_ORIG_pid'] = $wsAlt['pid'];
3963
                    // Set in the online versions PID.
3964
                    $wsAlt['pid'] = $row['pid'];
3965
                }
3966
                // For versions of single elements or page+content, swap UID and PID
3967
                $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
3968
                $wsAlt['uid'] = $row['uid'];
3969
                // Backend css class:
3970
                $wsAlt['_CSSCLASS'] = 'ver-element';
3971
                // Changing input record to the workspace version alternative:
3972
                $row = $wsAlt;
3973
            }
3974
            // If the original record was a move placeholder, the uid and pid of that is preserved here:
3975
            if ($movePldSwap) {
3976
                $row['_MOVE_PLH'] = true;
3977
                $row['_MOVE_PLH_uid'] = $orig_uid;
3978
                $row['_MOVE_PLH_pid'] = $orig_pid;
3979
                // For display; To make the icon right for the placeholder vs. the original
3980
                $row['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
3981
            }
3982
        }
3983
    }
3984
3985
    /**
3986
     * Checks if record is a move-placeholder (t3ver_state==VersionState::MOVE_PLACEHOLDER) and if so
3987
     * it will set $row to be the pointed-to live record (and return TRUE)
3988
     *
3989
     * @param string $table Table name
3990
     * @param array $row Row (passed by reference) - must be online record!
3991
     * @return bool TRUE if overlay is made.
3992
     * @see PageRepository::movePlhOl()
3993
     */
3994
    public static function movePlhOL($table, &$row)
3995
    {
3996
        if (static::isTableWorkspaceEnabled($table)) {
3997
            // If t3ver_move_id or t3ver_state is not found, then find it... (but we like best if it is here...)
3998
            if (!isset($row['t3ver_move_id']) || !isset($row['t3ver_state'])) {
3999
                $moveIDRec = self::getRecord($table, $row['uid'], 't3ver_move_id, t3ver_state');
4000
                $moveID = $moveIDRec['t3ver_move_id'];
4001
                $versionState = VersionState::cast($moveIDRec['t3ver_state']);
4002
            } else {
4003
                $moveID = $row['t3ver_move_id'];
4004
                $versionState = VersionState::cast($row['t3ver_state']);
4005
            }
4006
            // Find pointed-to record.
4007
            if ($versionState->equals(VersionState::MOVE_PLACEHOLDER) && $moveID) {
4008
                if ($origRow = self::getRecord(
4009
                    $table,
4010
                    $moveID,
4011
                    implode(',', static::purgeComputedPropertyNames(array_keys($row)))
4012
                )) {
4013
                    $row = $origRow;
4014
                    return true;
4015
                }
4016
            }
4017
        }
4018
        return false;
4019
    }
4020
4021
    /**
4022
     * Select the workspace version of a record, if exists
4023
     *
4024
     * @param int $workspace Workspace ID
4025
     * @param string $table Table name to select from
4026
     * @param int $uid Record uid for which to find workspace version.
4027
     * @param string $fields Field list to select
4028
     * @return array|bool If found, return record, otherwise false
4029
     */
4030
    public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*')
4031
    {
4032
        if (ExtensionManagementUtility::isLoaded('workspaces')) {
4033
            if ($workspace !== 0 && $GLOBALS['TCA'][$table] && self::isTableWorkspaceEnabled($table)) {
4034
4035
                // Select workspace version of record:
4036
                $queryBuilder = static::getQueryBuilderForTable($table);
4037
                $queryBuilder->getRestrictions()
4038
                    ->removeAll()
4039
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
4040
4041
                // build fields to select
4042
                $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields));
4043
4044
                $row = $queryBuilder
4045
                    ->from($table)
4046
                    ->where(
4047
                        $queryBuilder->expr()->eq(
4048
                            'pid',
4049
                            $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
4050
                        ),
4051
                        $queryBuilder->expr()->eq(
4052
                            't3ver_oid',
4053
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4054
                        ),
4055
                        $queryBuilder->expr()->eq(
4056
                            't3ver_wsid',
4057
                            $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
4058
                        )
4059
                    )
4060
                    ->execute()
4061
                    ->fetch();
4062
4063
                return $row;
4064
            }
4065
        }
4066
        return false;
4067
    }
4068
4069
    /**
4070
     * Returns live version of record
4071
     *
4072
     * @param string $table Table name
4073
     * @param int $uid Record UID of draft, offline version
4074
     * @param string $fields Field list, default is *
4075
     * @return array|null If found, the record, otherwise NULL
4076
     */
4077
    public static function getLiveVersionOfRecord($table, $uid, $fields = '*')
4078
    {
4079
        $liveVersionId = self::getLiveVersionIdOfRecord($table, $uid);
4080
        if ($liveVersionId !== null) {
4081
            return self::getRecord($table, $liveVersionId, $fields);
4082
        }
4083
        return null;
4084
    }
4085
4086
    /**
4087
     * Gets the id of the live version of a record.
4088
     *
4089
     * @param string $table Name of the table
4090
     * @param int $uid Uid of the offline/draft record
4091
     * @return int The id of the live version of the record (or NULL if nothing was found)
4092
     */
4093
    public static function getLiveVersionIdOfRecord($table, $uid)
4094
    {
4095
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
4096
            return null;
4097
        }
4098
        $liveVersionId = null;
4099
        if (self::isTableWorkspaceEnabled($table)) {
4100
            $currentRecord = self::getRecord($table, $uid, 'pid,t3ver_oid');
4101
            if (is_array($currentRecord) && $currentRecord['pid'] == -1) {
4102
                $liveVersionId = $currentRecord['t3ver_oid'];
4103
            }
4104
        }
4105
        return $liveVersionId;
4106
    }
4107
4108
    /**
4109
     * Will return where clause de-selecting new(/deleted)-versions from other workspaces.
4110
     * If in live-workspace, don't show "MOVE-TO-PLACEHOLDERS" records if versioningWS is 2 (allows moving)
4111
     *
4112
     * @param string $table Table name
4113
     * @return string Where clause if applicable.
4114
     */
4115
    public static function versioningPlaceholderClause($table)
4116
    {
4117
        if (static::isTableWorkspaceEnabled($table)) {
4118
            $currentWorkspace = (int)static::getBackendUserAuthentication()->workspace;
4119
            return ' AND (' . $table . '.t3ver_state <= ' . new VersionState(VersionState::DEFAULT_STATE) . ' OR ' . $table . '.t3ver_wsid = ' . $currentWorkspace . ')';
4120
        }
4121
        return '';
4122
    }
4123
4124
    /**
4125
     * Get additional where clause to select records of a specific workspace (includes live as well).
4126
     *
4127
     * @param string $table Table name
4128
     * @param int $workspaceId Workspace ID
4129
     * @return string Workspace where clause
4130
     */
4131
    public static function getWorkspaceWhereClause($table, $workspaceId = null)
4132
    {
4133
        $whereClause = '';
4134
        if (self::isTableWorkspaceEnabled($table)) {
4135
            if (is_null($workspaceId)) {
4136
                $workspaceId = static::getBackendUserAuthentication()->workspace;
4137
            }
4138
            $workspaceId = (int)$workspaceId;
4139
            $pidOperator = $workspaceId === 0 ? '!=' : '=';
4140
            $whereClause = ' AND ' . $table . '.t3ver_wsid=' . $workspaceId . ' AND ' . $table . '.pid' . $pidOperator . '-1';
4141
        }
4142
        return $whereClause;
4143
    }
4144
4145
    /**
4146
     * Performs mapping of new uids to new versions UID in case of import inside a workspace.
4147
     *
4148
     * @param string $table Table name
4149
     * @param int $uid Record uid (of live record placeholder)
4150
     * @return int Uid of offline version if any, otherwise live uid.
4151
     */
4152
    public static function wsMapId($table, $uid)
4153
    {
4154
        $wsRec = self::getWorkspaceVersionOfRecord(
4155
            static::getBackendUserAuthentication()->workspace,
4156
            $table,
4157
            $uid,
4158
            'uid'
4159
        );
4160
        return is_array($wsRec) ? $wsRec['uid'] : $uid;
4161
    }
4162
4163
    /**
4164
     * Returns move placeholder of online (live) version
4165
     *
4166
     * @param string $table Table name
4167
     * @param int $uid Record UID of online version
4168
     * @param string $fields Field list, default is *
4169
     * @param int|null $workspace The workspace to be used
4170
     * @return array|bool If found, the record, otherwise false
4171
     */
4172
    public static function getMovePlaceholder($table, $uid, $fields = '*', $workspace = null)
4173
    {
4174
        if ($workspace === null) {
4175
            $workspace = static::getBackendUserAuthentication()->workspace;
4176
        }
4177
        if ((int)$workspace !== 0 && $GLOBALS['TCA'][$table] && static::isTableWorkspaceEnabled($table)) {
4178
            // Select workspace version of record:
4179
            $queryBuilder = static::getQueryBuilderForTable($table);
4180
            $queryBuilder->getRestrictions()
4181
                ->removeAll()
4182
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
4183
4184
            $row = $queryBuilder
4185
                ->select(...GeneralUtility::trimExplode(',', $fields, true))
4186
                ->from($table)
4187
                ->where(
4188
                    $queryBuilder->expr()->neq(
4189
                        'pid',
4190
                        $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
4191
                    ),
4192
                    $queryBuilder->expr()->eq(
4193
                        't3ver_state',
4194
                        $queryBuilder->createNamedParameter(
4195
                            (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
4196
                            \PDO::PARAM_INT
4197
                        )
4198
                    ),
4199
                    $queryBuilder->expr()->eq(
4200
                        't3ver_move_id',
4201
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4202
                    ),
4203
                    $queryBuilder->expr()->eq(
4204
                        't3ver_wsid',
4205
                        $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
4206
                    )
4207
                )
4208
                ->execute()
4209
                ->fetch();
4210
4211
            return $row ?: false;
4212
        }
4213
        return false;
4214
    }
4215
4216
    /*******************************************
4217
     *
4218
     * Miscellaneous
4219
     *
4220
     *******************************************/
4221
    /**
4222
     * Prints TYPO3 Copyright notice for About Modules etc. modules.
4223
     *
4224
     * Warning:
4225
     * DO NOT prevent this notice from being shown in ANY WAY.
4226
     * According to the GPL license an interactive application must show such a notice on start-up ('If the program is interactive, make it output a short notice... ' - see GPL.txt)
4227
     * Therefore preventing this notice from being properly shown is a violation of the license, regardless of whether you remove it or use a stylesheet to obstruct the display.
4228
     *
4229
     * @return string Text/Image (HTML) for copyright notice.
4230
     */
4231
    public static function TYPO3_copyRightNotice()
4232
    {
4233
        // Copyright Notice
4234
        $loginCopyrightWarrantyProvider = strip_tags(trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyProvider']));
4235
        $loginCopyrightWarrantyURL = strip_tags(trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['loginCopyrightWarrantyURL']));
4236
4237
        $lang = static::getLanguageService();
4238
4239
        if (strlen($loginCopyrightWarrantyProvider) >= 2 && strlen($loginCopyrightWarrantyURL) >= 10) {
4240
            $warrantyNote = sprintf(
4241
                $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:warranty.by'),
4242
                htmlspecialchars($loginCopyrightWarrantyProvider),
4243
                '<a href="' . htmlspecialchars($loginCopyrightWarrantyURL) . '" target="_blank">',
4244
                '</a>'
4245
            );
4246
        } else {
4247
            $warrantyNote = sprintf(
4248
                $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:no.warranty'),
4249
                '<a href="' . TYPO3_URL_LICENSE . '" target="_blank">',
4250
                '</a>'
4251
            );
4252
        }
4253
        $cNotice = '<a href="' . TYPO3_URL_GENERAL . '" target="_blank">' .
4254
            $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:typo3.cms') . '</a>. ' .
4255
            $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:copyright') . ' &copy; '
4256
            . htmlspecialchars(TYPO3_copyright_year) . ' Kasper Sk&aring;rh&oslash;j. ' .
4257
            $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:extension.copyright') . ' ' .
4258
            sprintf(
4259
                $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:details.link'),
4260
                ('<a href="' . TYPO3_URL_GENERAL . '" target="_blank">' . TYPO3_URL_GENERAL . '</a>')
4261
            ) . ' ' .
4262
            strip_tags($warrantyNote, '<a>') . ' ' .
4263
            sprintf(
4264
                $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:free.software'),
4265
                ('<a href="' . TYPO3_URL_LICENSE . '" target="_blank">'),
4266
                '</a> '
4267
            )
4268
            . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_login.xlf:keep.notice');
4269
        return $cNotice;
4270
    }
4271
4272
    /**
4273
     * Creates ADMCMD parameters for the "viewpage" extension / frontend
4274
     *
4275
     * @param array $pageInfo Page record
4276
     * @return string Query-parameters
4277
     * @internal
4278
     */
4279
    public static function ADMCMD_previewCmds($pageInfo)
4280
    {
4281
        $simUser = '';
4282
        $simTime = '';
4283
        if ($pageInfo['fe_group'] > 0) {
4284
            $simUser = '&ADMCMD_simUser=' . $pageInfo['fe_group'];
4285
        } elseif ((int)$pageInfo['fe_group'] === -2) {
4286
            // -2 means "show at any login". We simulate first available fe_group.
4287
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
4288
                ->getQueryBuilderForTable('fe_groups');
4289
            $queryBuilder->getRestrictions()
4290
                ->removeAll()
4291
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4292
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
4293
4294
            $activeFeGroupRow = $queryBuilder->select('uid')
4295
                ->from('fe_groups')
4296
                ->execute()
4297
                ->fetch();
4298
4299
            if (!empty($activeFeGroupRow)) {
4300
                $simUser = '&ADMCMD_simUser=' . $activeFeGroupRow['uid'];
4301
            }
4302
        }
4303
        if ($pageInfo['starttime'] > $GLOBALS['EXEC_TIME']) {
4304
            $simTime = '&ADMCMD_simTime=' . $pageInfo['starttime'];
4305
        }
4306
        if ($pageInfo['endtime'] < $GLOBALS['EXEC_TIME'] && $pageInfo['endtime'] != 0) {
4307
            $simTime = '&ADMCMD_simTime=' . ($pageInfo['endtime'] - 1);
4308
        }
4309
        return $simUser . $simTime;
4310
    }
4311
4312
    /**
4313
     * Returns the name of the backend script relative to the TYPO3 main directory.
4314
     *
4315
     * @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.
4316
     * @return string The name of the backend script relative to the TYPO3 main directory.
4317
     */
4318
    public static function getBackendScript($interface = '')
4319
    {
4320
        if (!$interface) {
4321
            $interface = static::getBackendUserAuthentication()->uc['interfaceSetup'];
4322
        }
4323
        switch ($interface) {
4324
            case 'frontend':
4325
                $script = '../.';
4326
                break;
4327
            case 'backend':
4328
            default:
4329
                $script = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('main');
4330
        }
4331
        return $script;
4332
    }
4333
4334
    /**
4335
     * Determines whether a table is enabled for workspaces.
4336
     *
4337
     * @param string $table Name of the table to be checked
4338
     * @return bool
4339
     */
4340
    public static function isTableWorkspaceEnabled($table)
4341
    {
4342
        return !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']);
4343
    }
4344
4345
    /**
4346
     * Gets the TCA configuration of a field.
4347
     *
4348
     * @param string $table Name of the table
4349
     * @param string $field Name of the field
4350
     * @return array
4351
     */
4352
    public static function getTcaFieldConfiguration($table, $field)
4353
    {
4354
        $configuration = [];
4355
        if (isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4356
            $configuration = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4357
        }
4358
        return $configuration;
4359
    }
4360
4361
    /**
4362
     * Whether to ignore restrictions on a web-mount of a table.
4363
     * The regular behaviour is that records to be accessed need to be
4364
     * in a valid user's web-mount.
4365
     *
4366
     * @param string $table Name of the table
4367
     * @return bool
4368
     */
4369
    public static function isWebMountRestrictionIgnored($table)
4370
    {
4371
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreWebMountRestriction']);
4372
    }
4373
4374
    /**
4375
     * Whether to ignore restrictions on root-level records.
4376
     * The regular behaviour is that records on the root-level (page-id 0)
4377
     * only can be accessed by admin users.
4378
     *
4379
     * @param string $table Name of the table
4380
     * @return bool
4381
     */
4382
    public static function isRootLevelRestrictionIgnored($table)
4383
    {
4384
        return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']);
4385
    }
4386
4387
    /**
4388
     * Exists already a shortcut entry for this TYPO3 url?
4389
     *
4390
     * @param string $url
4391
     *
4392
     * @return bool
4393
     */
4394
    public static function shortcutExists($url)
4395
    {
4396
        $queryBuilder = static::getQueryBuilderForTable('sys_be_shortcuts');
4397
        $queryBuilder->getRestrictions()->removeAll();
4398
4399
        $count = $queryBuilder
4400
            ->count('uid')
4401
            ->from('sys_be_shortcuts')
4402
            ->where(
4403
                $queryBuilder->expr()->eq(
4404
                    'userid',
4405
                    $queryBuilder->createNamedParameter(
4406
                        self::getBackendUserAuthentication()->user['uid'],
4407
                        \PDO::PARAM_INT
4408
                    )
4409
                ),
4410
                $queryBuilder->expr()->eq('url', $queryBuilder->createNamedParameter($url, \PDO::PARAM_STR))
4411
            )
4412
            ->execute()
4413
            ->fetchColumn(0);
4414
4415
        return (bool)$count;
4416
    }
4417
4418
    /**
4419
     * Get the SignalSlot dispatcher
4420
     *
4421
     * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
4422
     */
4423
    protected static function getSignalSlotDispatcher()
4424
    {
4425
        return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class);
4426
    }
4427
4428
    /**
4429
     * Emits signal to modify the page TSconfig before include
4430
     *
4431
     * @param array $TSdataArray Current TSconfig data array - Can be modified by slots!
4432
     * @param int $id Page ID we are handling
4433
     * @param array $rootLine Rootline array of page
4434
     * @return array Modified Data array
4435
     */
4436
    protected static function emitGetPagesTSconfigPreIncludeSignal(
4437
        array $TSdataArray,
4438
        $id,
4439
        array $rootLine
4440
    ) {
4441
        $signalArguments = static::getSignalSlotDispatcher()->dispatch(
4442
            __CLASS__,
4443
            'getPagesTSconfigPreInclude',
4444
            [$TSdataArray, $id, $rootLine, false]
4445
        );
4446
        return $signalArguments[0];
4447
    }
4448
4449
    /**
4450
     * @param string $table
4451
     * @return Connection
4452
     */
4453
    protected static function getConnectionForTable($table)
4454
    {
4455
        return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
4456
    }
4457
4458
    /**
4459
     * @param string $table
4460
     * @return QueryBuilder
4461
     */
4462
    protected static function getQueryBuilderForTable($table)
4463
    {
4464
        return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4465
    }
4466
4467
    /**
4468
     * @return LoggerInterface
4469
     */
4470
    protected static function getLogger()
4471
    {
4472
        return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
4473
    }
4474
4475
    /**
4476
     * @return LanguageService
4477
     */
4478
    protected static function getLanguageService()
4479
    {
4480
        return $GLOBALS['LANG'];
4481
    }
4482
4483
    /**
4484
     * @return BackendUserAuthentication
4485
     */
4486
    protected static function getBackendUserAuthentication()
4487
    {
4488
        return $GLOBALS['BE_USER'];
4489
    }
4490
}
4491